feat(csharp): add consumer and publisher clients (#2259)
- Add IggyPublisher for sending messages:
- Create stream and topic
- Send direct or in background
- Generic version with serializer
- Add IggyConsumer for polling messages:
- Create and join consumer group
- Store offset support
- IAsyncEnumerable support
- Generic version with deserializer
- Add builders for both clients
- Implement AES encryptor
- Change `Message` from readonly struct to class
---------
Co-authored-by: Piotr Gankiewicz <piotr.gankiewicz@gmail.com>
diff --git a/.gitignore b/.gitignore
index 0bde9d8..4373789 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,7 +25,7 @@
foreign/node/**/dist
foreign/node/npm-debug.log*
foreign/node/.npm
-foreign/csharp/**/*.DotSettings.user
+/**/*.DotSettings.user
*.exe
*.exe~
*.dll
diff --git a/examples/csharp/.editorconfig b/examples/csharp/.editorconfig
new file mode 100644
index 0000000..7fb91e9
--- /dev/null
+++ b/examples/csharp/.editorconfig
@@ -0,0 +1,175 @@
+[*]
+charset = utf-8
+end_of_line = crlf
+trim_trailing_whitespace = false
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+# Microsoft .NET properties
+csharp_preferred_modifier_order = public, private, protected, internal, file, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async, required:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+dotnet_naming_rule.constants_rule.import_to_resharper = True
+dotnet_naming_rule.constants_rule.resharper_description = Constant fields (not private)
+dotnet_naming_rule.constants_rule.resharper_guid = 669e5282-fb4b-4e90-91e7-07d269d04b60
+dotnet_naming_rule.constants_rule.severity = warning
+dotnet_naming_rule.constants_rule.style = all_upper_style
+dotnet_naming_rule.constants_rule.symbols = constants_symbols
+dotnet_naming_rule.interfaces_rule.import_to_resharper = True
+dotnet_naming_rule.interfaces_rule.resharper_description = Interfaces
+dotnet_naming_rule.interfaces_rule.resharper_guid = a7a3339e-4e89-4319-9735-a9dc4cb74cc7
+dotnet_naming_rule.interfaces_rule.severity = warning
+dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style
+dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
+dotnet_naming_rule.unity_serialized_field_rule.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule.style = lower_camel_case_style_1
+dotnet_naming_rule.unity_serialized_field_rule.symbols = unity_serialized_field_symbols
+dotnet_naming_rule.unity_serialized_field_rule_1.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule_1.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule_1.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule_1.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule_1.style = lower_camel_case_style_1
+dotnet_naming_rule.unity_serialized_field_rule_1.symbols = unity_serialized_field_symbols_1
+dotnet_naming_rule.unity_serialized_field_rule_2.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule_2.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule_2.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule_2.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule_2.style = lower_camel_case_style_1
+dotnet_naming_rule.unity_serialized_field_rule_2.symbols = unity_serialized_field_symbols_2
+dotnet_naming_rule.unity_serialized_field_rule_3.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule_3.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule_3.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule_3.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule_3.style = lower_camel_case_style_1
+dotnet_naming_rule.unity_serialized_field_rule_3.symbols = unity_serialized_field_symbols_3
+dotnet_naming_rule.unity_serialized_field_rule_4.import_to_resharper = True
+dotnet_naming_rule.unity_serialized_field_rule_4.resharper_description = Unity serialized field
+dotnet_naming_rule.unity_serialized_field_rule_4.resharper_guid = 5f0fdb63-c892-4d2c-9324-15c80b22a7ef
+dotnet_naming_rule.unity_serialized_field_rule_4.severity = warning
+dotnet_naming_rule.unity_serialized_field_rule_4.style = lower_camel_case_style_1
+dotnet_naming_rule.unity_serialized_field_rule_4.symbols = unity_serialized_field_symbols_4
+dotnet_naming_style.all_upper_style.capitalization = all_upper
+dotnet_naming_style.all_upper_style.word_separator = _
+dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case
+dotnet_naming_style.i_upper_camel_case_style.required_prefix = I
+dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
+dotnet_naming_style.lower_camel_case_style.required_prefix = _
+dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case
+dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
+dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.constants_symbols.required_modifiers = const
+dotnet_naming_symbols.constants_symbols.resharper_applicable_kinds = constant_field
+dotnet_naming_symbols.constants_symbols.resharper_required_modifiers = any
+dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
+dotnet_naming_symbols.interfaces_symbols.resharper_applicable_kinds = interface
+dotnet_naming_symbols.interfaces_symbols.resharper_required_modifiers = any
+dotnet_naming_symbols.unity_serialized_field_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols.resharper_required_modifiers = instance
+dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance
+dotnet_naming_symbols.unity_serialized_field_symbols_2.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols_2.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols_2.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols_2.resharper_required_modifiers = instance
+dotnet_naming_symbols.unity_serialized_field_symbols_3.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols_3.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols_3.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols_3.resharper_required_modifiers = instance
+dotnet_naming_symbols.unity_serialized_field_symbols_4.applicable_accessibilities = *
+dotnet_naming_symbols.unity_serialized_field_symbols_4.applicable_kinds =
+dotnet_naming_symbols.unity_serialized_field_symbols_4.resharper_applicable_kinds = unity_serialised_field
+dotnet_naming_symbols.unity_serialized_field_symbols_4.resharper_required_modifiers = instance
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+
+# ReSharper properties
+resharper_autodetect_indent_settings = true
+resharper_braces_for_for = required
+resharper_braces_for_foreach = required
+resharper_braces_for_ifelse = required
+resharper_braces_for_while = required
+resharper_cpp_insert_final_newline = true
+resharper_csharp_stick_comment = false
+resharper_csharp_wrap_before_first_type_parameter_constraint = true
+resharper_formatter_off_tag = @formatter:off
+resharper_formatter_on_tag = @formatter:on
+resharper_formatter_tags_enabled = true
+resharper_for_other_types = use_var_when_evident
+resharper_keep_existing_declaration_parens_arrangement = false
+resharper_keep_existing_initializer_arrangement = false
+resharper_keep_existing_invocation_parens_arrangement = false
+resharper_max_array_initializer_elements_on_line = 10
+resharper_max_initializer_elements_on_line = 1
+resharper_max_invocation_arguments_on_line = 10
+resharper_place_accessorholder_attribute_on_same_line = false
+resharper_use_indent_from_vs = false
+resharper_wrap_before_eq = true
+resharper_xmldoc_space_after_last_pi_attribute = true
+
+# ReSharper inspection severities
+resharper_arrange_redundant_parentheses_highlighting = hint
+resharper_arrange_this_qualifier_highlighting = hint
+resharper_arrange_type_member_modifiers_highlighting = hint
+resharper_arrange_type_modifiers_highlighting = hint
+resharper_built_in_type_reference_style_for_member_access_highlighting = hint
+resharper_built_in_type_reference_style_highlighting = hint
+resharper_check_namespace_highlighting = error
+resharper_mvc_action_not_resolved_highlighting = warning
+resharper_mvc_area_not_resolved_highlighting = warning
+resharper_mvc_controller_not_resolved_highlighting = warning
+resharper_mvc_masterpage_not_resolved_highlighting = warning
+resharper_mvc_partial_view_not_resolved_highlighting = warning
+resharper_mvc_template_not_resolved_highlighting = warning
+resharper_mvc_view_component_not_resolved_highlighting = warning
+resharper_mvc_view_component_view_not_resolved_highlighting = warning
+resharper_mvc_view_not_resolved_highlighting = warning
+resharper_razor_assembly_not_resolved_highlighting = warning
+resharper_redundant_base_qualifier_highlighting = warning
+resharper_suggest_var_or_type_simple_types_highlighting = suggestion
+resharper_web_config_module_not_resolved_highlighting = warning
+resharper_web_config_type_not_resolved_highlighting = warning
+resharper_web_config_wrong_module_highlighting = warning
+
+[{*.yaml,*.yml}]
+indent_style = space
+indent_size = 2
+
+[{*.bash,*.sh,*.zsh}]
+indent_style = space
+indent_size = 2
+
+[{*.har,*.jsb2,*.jsb3,*.json,*.jsonc,*.postman_collection,*.postman_collection.json,*.postman_environment,*.postman_environment.json,*.sonarlint/*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,.ws-context,connectedMode.json,jest.config}]
+indent_style = space
+indent_size = 2
+
+[{tsconfig.app.json,tsconfig.base.json,tsconfig.e2e.json,tsconfig.editor.json,tsconfig.json,tsconfig.lib.json,tsconfig.spec.json,tsconfig.test.json}]
+indent_style = space
+indent_size = 2
+
+[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,build,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}]
+indent_style = space
+indent_size = 4
+tab_width = 4
+
+[*.{appxmanifest,asax,ascx,aspx,axaml,blockshader,c,c++,c++m,cc,ccm,cginc,compute,cp,cpp,cppm,cs,cshtml,cu,cuh,cxx,cxxm,dtd,feature,fs,fsi,fsscript,fsx,fx,fxh,h,h++,hh,hlsl,hlsli,hlslinc,hp,hpp,hxx,icc,inc,inl,ino,ipp,ixx,master,ml,mli,mpp,mq4,mq5,mqh,mxx,nuspec,paml,razor,resw,resx,shader,shaderFoundry,skin,tcc,tpp,urtshader,usf,ush,uxml,vb,xaml,xamlx,xoml,xsd}]
+indent_style = space
+indent_size = 4
+tab_width = 4
diff --git a/examples/csharp/Iggy_SDK.Examples.sln b/examples/csharp/Iggy_SDK.Examples.sln
index fcf6d55..79e94a6 100644
--- a/examples/csharp/Iggy_SDK.Examples.sln
+++ b/examples/csharp/Iggy_SDK.Examples.sln
@@ -32,6 +32,12 @@
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Iggy_SDK.Examples.MessageHeaders.Producer", "src\MessageHeaders\Iggy_SDK.Examples.MessageHeaders.Producer\Iggy_SDK.Examples.MessageHeaders.Producer.csproj", "{FBA6F7E6-D92E-4E8F-81F0-625AE60F7E45}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NewSdk", "NewSdk", "{85F31923-45DF-4AEA-A28E-D37CF8E61A78}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Iggy_SDK.Examples.NewSdk.Consumer", "src\NewSdk\Iggy_SDK.Examples.NewSdk.Consumer\Iggy_SDK.Examples.NewSdk.Consumer.csproj", "{762203E3-A41B-414A-93BF-B963EAE447D1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Iggy_SDK.Examples.NewSdk.Producer", "src\NewSdk\Iggy_SDK.Examples.NewSdk.Producer\Iggy_SDK.Examples.NewSdk.Producer.csproj", "{DB9EAC21-61C8-449A-95A9-6DC369B15C02}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -81,6 +87,14 @@
{FBA6F7E6-D92E-4E8F-81F0-625AE60F7E45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FBA6F7E6-D92E-4E8F-81F0-625AE60F7E45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FBA6F7E6-D92E-4E8F-81F0-625AE60F7E45}.Release|Any CPU.Build.0 = Release|Any CPU
+ {762203E3-A41B-414A-93BF-B963EAE447D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {762203E3-A41B-414A-93BF-B963EAE447D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {762203E3-A41B-414A-93BF-B963EAE447D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {762203E3-A41B-414A-93BF-B963EAE447D1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB9EAC21-61C8-449A-95A9-6DC369B15C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB9EAC21-61C8-449A-95A9-6DC369B15C02}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB9EAC21-61C8-449A-95A9-6DC369B15C02}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB9EAC21-61C8-449A-95A9-6DC369B15C02}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{6CA0C169-109F-4A43-A21F-0792C1F865C8} = {3576C181-8BD5-4845-9CDA-835B33A29A29}
@@ -96,6 +110,9 @@
{578335B4-7B95-41D5-BB73-E965CAB2DBA4} = {3576C181-8BD5-4845-9CDA-835B33A29A29}
{0A09EEAA-DDAE-44D3-926C-88910D661901} = {578335B4-7B95-41D5-BB73-E965CAB2DBA4}
{FBA6F7E6-D92E-4E8F-81F0-625AE60F7E45} = {578335B4-7B95-41D5-BB73-E965CAB2DBA4}
+ {85F31923-45DF-4AEA-A28E-D37CF8E61A78} = {3576C181-8BD5-4845-9CDA-835B33A29A29}
+ {762203E3-A41B-414A-93BF-B963EAE447D1} = {85F31923-45DF-4AEA-A28E-D37CF8E61A78}
+ {DB9EAC21-61C8-449A-95A9-6DC369B15C02} = {85F31923-45DF-4AEA-A28E-D37CF8E61A78}
EndGlobalSection
EndGlobal
diff --git a/examples/csharp/Iggy_SDK.Examples.sln.DotSettings b/examples/csharp/Iggy_SDK.Examples.sln.DotSettings
new file mode 100644
index 0000000..3ea9d2f
--- /dev/null
+++ b/examples/csharp/Iggy_SDK.Examples.sln.DotSettings
@@ -0,0 +1,301 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceIfStatementBraces/@EntryIndexedValue"></s:String>
+ <s:Boolean x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceIfStatementBraces/@EntryIndexRemoved">True</s:Boolean>
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceUsingStatementBraces/@EntryIndexedValue"></s:String>
+ <s:Boolean x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=EnforceUsingStatementBraces/@EntryIndexRemoved">True</s:Boolean>
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SuggestVarOrType_005FBuiltInTypes/@EntryIndexedValue">SUGGESTION</s:String>
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SuggestVarOrType_005FElsewhere/@EntryIndexedValue">SUGGESTION</s:String>
+ <s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=SuggestVarOrType_005FSimpleTypes/@EntryIndexedValue">SUGGESTION</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOR/@EntryValue">Required</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_FOREACH/@EntryValue">Required</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_IFELSE/@EntryValue">Required</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_USING/@EntryValue">Required</s:String>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/BRACES_FOR_WHILE/@EntryValue">Required</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/INDENT_BRACES_INSIDE_STATEMENT_CONDITIONS/@EntryValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_DECLARATION_PARENS_ARRANGEMENT/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INITIALIZER_ARRANGEMENT/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_INVOCATION_PARENS_ARRANGEMENT/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_EXISTING_PRIMARY_CONSTRUCTOR_DECLARATION_PARENS_ARRANGEMENT/@EntryValue">True</s:Boolean>
+ <s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ARRAY_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">10</s:Int64>
+ <s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_INITIALIZER_ELEMENTS_ON_LINE/@EntryValue">1</s:Int64>
+ <s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_INVOCATION_ARGUMENTS_ON_LINE/@EntryValue">10</s:Int64>
+ <s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_ACCESSORHOLDER_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">NEVER</s:String>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/STICK_COMMENT/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_EQ/@EntryValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_FIRST_TYPE_PARAMETER_CONSTRAINT/@EntryValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_BEFORE_PRIMARY_CONSTRUCTOR_DECLARATION_LPAR/@EntryValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_LINES/@EntryValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/XmlDocFormatter/ProcessingInstructionSpaceAfterLastAttr/@EntryValue">True</s:Boolean>
+ <s:String x:Key="/Default/CodeStyle/CSharpFileLayoutPatterns/Pattern/@EntryValue"><Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="Non-reorderable types" Priority="99999999">
+ <TypePattern.Match>
+ <Or>
+ <And>
+ <Kind Is="Interface" />
+ <Or>
+ <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
+ <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
+ </Or>
+ </And>
+ <Kind Is="Struct" />
+ <HasAttribute Name="System.Runtime.InteropServices.StructLayoutAttribute" />
+ <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" />
+ </Or>
+ </TypePattern.Match>
+ </TypePattern>
+
+ <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <HasMember>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="Xunit.FactAttribute" Inherited="True" />
+ <HasAttribute Name="Xunit.TheoryAttribute" Inherited="True" />
+ </And>
+ </HasMember>
+ </And>
+ </TypePattern.Match>
+
+ <Entry DisplayName="Fields">
+ <Entry.Match>
+ <And>
+ <Kind Is="Field" />
+ <Not>
+ <Static />
+ </Not>
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Constructors">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Static/>
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <ImplementsInterface Name="System.IDisposable" />
+ </And>
+ </Entry.Match>
+ </Entry>
+
+ <Entry DisplayName="All other members" />
+
+ <Entry DisplayName="Test Methods" Priority="100">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="Xunit.FactAttribute" Inherited="false" />
+ <HasAttribute Name="Xunit.TheoryAttribute" Inherited="false" />
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+
+ <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="true" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureSourceAttribute" Inherited="true" />
+ <HasMember>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" Inherited="false" />
+ <HasAttribute Name="NUnit.Framework.TestCaseAttribute" Inherited="false" />
+ <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" Inherited="false" />
+ </And>
+ </HasMember>
+ </Or>
+ </And>
+ </TypePattern.Match>
+
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="true" />
+ <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="true" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureSetUpAttribute" Inherited="true" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureTearDownAttribute" Inherited="true" />
+ <HasAttribute Name="NUnit.Framework.OneTimeSetUpAttribute" Inherited="true" />
+ <HasAttribute Name="NUnit.Framework.OneTimeTearDownAttribute" Inherited="true" />
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+
+ <Entry DisplayName="All other members" />
+
+ <Entry DisplayName="Test Methods" Priority="100">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" Inherited="false" />
+ <HasAttribute Name="NUnit.Framework.TestCaseAttribute" Inherited="false" />
+ <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" Inherited="false" />
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+
+ <TypePattern DisplayName="Default Pattern">
+ <Entry DisplayName="Public Delegates" Priority="100">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Delegate" />
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Public Enums" Priority="100">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Enum" />
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Static Fields and Constants">
+ <Entry.Match>
+ <Or>
+ <Kind Is="Constant" />
+ <And>
+ <Kind Is="Field" />
+ <Static />
+ </And>
+ </Or>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Kind>
+ <Kind.Order>
+ <DeclarationKind>Constant</DeclarationKind>
+ <DeclarationKind>Field</DeclarationKind>
+ </Kind.Order>
+ </Kind>
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Fields">
+ <Entry.Match>
+ <And>
+ <Kind Is="Field" />
+ <Not>
+ <Static />
+ </Not>
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Properties, Indexers">
+ <Entry.Match>
+ <Or>
+ <Kind Is="Property" />
+ <Kind Is="Indexer" />
+ </Or>
+ </Entry.Match>
+ </Entry>
+
+ <Entry DisplayName="Constructors">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <Static/>
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="Interface Implementations" Priority="100">
+ <Entry.Match>
+ <And>
+ <Kind Is="Member" />
+ <ImplementsInterface />
+ </And>
+ </Entry.Match>
+
+ <Entry.SortBy>
+ <ImplementsInterface Immediate="true" />
+ </Entry.SortBy>
+ </Entry>
+
+ <Entry DisplayName="All other members" />
+
+ <Entry DisplayName="Nested Types">
+ <Entry.Match>
+ <Kind Is="Type" />
+ </Entry.Match>
+ </Entry>
+ </TypePattern>
+</Patterns>
+</s:String>
+ <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForBuiltInTypes/@EntryValue">UseVar</s:String>
+ <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseVarWhenEvident</s:String>
+ <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseVar</s:String>
+ <s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.</s:String>
+ <s:String x:Key="/Default/Environment/Hierarchy/Build/SolBuilderDuo/CustomGlobalProperties/=PreferredUILang/@EntryIndexedValue">en-US</s:String>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002ECodeCleanup_002EFileHeader_002EFileHeaderSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002EMemberReordering_002EMigrations_002ECSharpFileLayoutPatternRemoveIsAttributeUpgrade/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EUnitTestFramework_002EMigrations_002EEnableDisabledProvidersMigration/@EntryIndexedValue">True</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/UnitTesting/DisabledProviders/=Testing_0020Platform/@EntryIndexedValue">False</s:Boolean>
+ <s:Boolean x:Key="/Default/Environment/UnitTesting/DisabledProviders/=VsTest/@EntryIndexedValue">True</s:Boolean>
+ </wpf:ResourceDictionary>
diff --git a/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Consumer/Program.cs b/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Consumer/Program.cs
index daef1f6..a288204 100644
--- a/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Consumer/Program.cs
+++ b/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Consumer/Program.cs
@@ -17,6 +17,7 @@
using System.Text;
using Apache.Iggy;
+using Apache.Iggy.Configuration;
using Apache.Iggy.Factory;
using Apache.Iggy.Kinds;
using Iggy_SDK.Examples.Basic.Consumer;
@@ -35,14 +36,12 @@
settings.Protocol
);
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = settings.BaseAddress;
- opt.Protocol = settings.Protocol;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = settings.BaseAddress,
+ Protocol = settings.Protocol,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser(settings.Username, settings.Password);
diff --git a/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Producer/Program.cs b/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Producer/Program.cs
index 25276b5..fa23193 100644
--- a/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Producer/Program.cs
+++ b/examples/csharp/src/Basic/Iggy_SDK.Examples.Basic.Producer/Program.cs
@@ -17,6 +17,7 @@
using System.Text;
using Apache.Iggy;
+using Apache.Iggy.Configuration;
using Apache.Iggy.Contracts;
using Apache.Iggy.Factory;
using Apache.Iggy.Kinds;
@@ -37,14 +38,12 @@
settings.Protocol
);
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = settings.BaseAddress;
- opt.Protocol = settings.Protocol;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = settings.BaseAddress,
+ Protocol = settings.Protocol,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser(settings.Username, settings.Password);
@@ -82,15 +81,7 @@
var messages = payloads.Select(payload => new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes(payload))).ToList();
- await client.SendMessagesAsync(
- new MessageSendRequest
- {
- StreamId = streamId,
- TopicId = topicId,
- Messages = messages,
- Partitioning = Partitioning.None()
- }
- );
+ await client.SendMessagesAsync(streamId, topicId, Partitioning.None(), messages);
currentId += settings.MessagesPerBatch;
sentBatches++;
diff --git a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Program.cs b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Program.cs
index b0f9b13..deb1ae1 100644
--- a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Program.cs
+++ b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Program.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Iggy_SDK.Examples.GettingStarted.Consumer;
@@ -23,14 +24,12 @@
var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
var logger = loggerFactory.CreateLogger<Program>();
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = Utils.GetTcpServerAddr(args, logger);
- opt.Protocol = Protocol.Tcp;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser("iggy", "iggy");
diff --git a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Utils.cs b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Utils.cs
index 4245edf..bb39920 100644
--- a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Utils.cs
+++ b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Consumer/Utils.cs
@@ -44,7 +44,7 @@
);
var offset = 0ul;
- var messagesPerBatch = 10;
+ uint messagesPerBatch = 10;
var consumedBatches = 0;
var consumer = Apache.Iggy.Kinds.Consumer.New(1);
while (true)
@@ -104,14 +104,20 @@
argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
if (argumentName != "--tcp-server-address")
+ {
throw new FormatException(
$"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
);
+ }
+
tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
throw new FormatException(
$"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
);
+ }
+
logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
return tcpServerAddr;
}
diff --git a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Program.cs b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Program.cs
index 8b62963..125215e 100644
--- a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Program.cs
+++ b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Program.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Iggy_SDK.Examples.GettingStarted.Producer;
@@ -23,14 +24,12 @@
var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
var logger = loggerFactory.CreateLogger<Program>();
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = Utils.GetTcpServerAddr(args, logger);
- opt.Protocol = Protocol.Tcp;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser("iggy", "iggy");
diff --git a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Utils.cs b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Utils.cs
index c1fe3f6..482221e 100644
--- a/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Utils.cs
+++ b/examples/csharp/src/GettingStarted/Iggy_SDK.Examples.GettingStarted.Producer/Utils.cs
@@ -103,15 +103,7 @@
var streamIdentifier = Identifier.Numeric(STREAM_ID);
var topicIdentifier = Identifier.Numeric(TOPIC_ID);
- await client.SendMessagesAsync(
- new MessageSendRequest
- {
- StreamId = streamIdentifier,
- TopicId = topicIdentifier,
- Partitioning = partitioning,
- Messages = messages
- }
- );
+ await client.SendMessagesAsync(streamIdentifier, topicIdentifier, partitioning, messages);
currentId += messagesPerBatch;
sentBatches++;
@@ -131,14 +123,20 @@
argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
if (argumentName != "--tcp-server-address")
+ {
throw new FormatException(
$"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
);
+ }
+
tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
throw new FormatException(
$"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
);
+ }
+
logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
return tcpServerAddr;
}
diff --git a/examples/csharp/src/Iggy_SDK.Examples.Shared/ExampleHelpers.cs b/examples/csharp/src/Iggy_SDK.Examples.Shared/ExampleHelpers.cs
index cb9c5b2..f9e36c6 100644
--- a/examples/csharp/src/Iggy_SDK.Examples.Shared/ExampleHelpers.cs
+++ b/examples/csharp/src/Iggy_SDK.Examples.Shared/ExampleHelpers.cs
@@ -30,11 +30,8 @@
CancellationToken token = default
)
{
- try
- {
- await client.GetStreamByIdAsync(streamId, token);
- }
- catch (InvalidResponseException)
+ var stream = await client.GetStreamByIdAsync(streamId, token);
+ if(stream == null)
{
await client.CreateStreamAsync(streamName, token: token);
}
@@ -49,11 +46,8 @@
CancellationToken cancellationToken = default
)
{
- try
- {
- await client.GetTopicByIdAsync(streamId, topicId, cancellationToken);
- }
- catch (InvalidResponseException)
+ var topic = await client.GetTopicByIdAsync(streamId, topicId, cancellationToken);
+ if(topic == null)
{
await client.CreateTopicAsync(
streamId,
diff --git a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Program.cs b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Program.cs
index c55e648..2192003 100644
--- a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Program.cs
+++ b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Program.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Iggy_SDK.Examples.MessageEnvelope.Consumer;
@@ -23,14 +24,12 @@
var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
var logger = loggerFactory.CreateLogger<Program>();
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = Utils.GetTcpServerAddr(args, logger);
- opt.Protocol = Protocol.Tcp;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser("iggy", "iggy");
diff --git a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Utils.cs b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Utils.cs
index 908c2a1..bdebc69 100644
--- a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Utils.cs
+++ b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Consumer/Utils.cs
@@ -46,7 +46,7 @@
);
var offset = 0ul;
- var messagesPerBatch = 10;
+ uint messagesPerBatch = 10;
var consumedBatches = 0;
var consumer = Apache.Iggy.Kinds.Consumer.New(1);
while (true)
@@ -132,14 +132,20 @@
argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
if (argumentName != "--tcp-server-address")
+ {
throw new FormatException(
$"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
);
+ }
+
tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
throw new FormatException(
$"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
);
+ }
+
logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
return tcpServerAddr;
}
diff --git a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Program.cs b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Program.cs
index 193fc81..02e45b6 100644
--- a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Program.cs
+++ b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Program.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Iggy_SDK.Examples.MessageEnvelope.Producer;
@@ -23,14 +24,12 @@
var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
var logger = loggerFactory.CreateLogger<Program>();
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = Utils.GetTcpServerAddr(args, logger);
- opt.Protocol = Protocol.Tcp;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser("iggy", "iggy");
diff --git a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Utils.cs b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Utils.cs
index 82f2c23..ad1f34f 100644
--- a/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Utils.cs
+++ b/examples/csharp/src/MessageEnvelope/Iggy_SDK.Examples.MessageEnvelope.Producer/Utils.cs
@@ -112,15 +112,7 @@
var topicIdentifier = Identifier.Numeric(TOPIC_ID);
logger.LogInformation("Sending messages count: {Count}", messagesPerBatch);
- await client.SendMessagesAsync(
- new MessageSendRequest
- {
- StreamId = streamIdentifier,
- TopicId = topicIdentifier,
- Partitioning = partitioning,
- Messages = messages
- }
- );
+ await client.SendMessagesAsync(streamIdentifier, topicIdentifier, partitioning, messages);
sentBatches++;
logger.LogInformation("Sent messages: {Messages}.", serializableMessages);
@@ -139,14 +131,20 @@
argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
if (argumentName != "--tcp-server-address")
+ {
throw new FormatException(
$"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
);
+ }
+
tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
throw new FormatException(
$"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
);
+ }
+
logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
return tcpServerAddr;
}
diff --git a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Program.cs b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Program.cs
index f53d547..ac6d114 100644
--- a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Program.cs
+++ b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Program.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Iggy_SDK.Examples.MessageHeaders.Consumer;
@@ -23,14 +24,12 @@
var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
var logger = loggerFactory.CreateLogger<Program>();
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = Utils.GetTcpServerAddr(args, logger);
- opt.Protocol = Protocol.Tcp;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser("iggy", "iggy");
diff --git a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Utils.cs b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Utils.cs
index 770d11b..ae03bca 100644
--- a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Utils.cs
+++ b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Consumer/Utils.cs
@@ -47,7 +47,7 @@
);
var offset = 0ul;
- var messagesPerBatch = 10;
+ uint messagesPerBatch = 10;
var consumedBatches = 0;
var consumer = Apache.Iggy.Kinds.Consumer.New(1);
while (true)
@@ -133,14 +133,20 @@
argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
if (argumentName != "--tcp-server-address")
+ {
throw new FormatException(
$"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
);
+ }
+
tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
throw new FormatException(
$"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
);
+ }
+
logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
return tcpServerAddr;
}
diff --git a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Program.cs b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Program.cs
index f265e74..0fbb1c3 100644
--- a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Program.cs
+++ b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Program.cs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Iggy_SDK.Examples.MessageHeaders.Producer;
@@ -23,14 +24,12 @@
var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
var logger = loggerFactory.CreateLogger<Program>();
-var client = MessageStreamFactory.CreateMessageStream(
- opt =>
- {
- opt.BaseAdress = Utils.GetTcpServerAddr(args, logger);
- opt.Protocol = Protocol.Tcp;
- },
- loggerFactory
-);
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
await client.LoginUser("iggy", "iggy");
diff --git a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Utils.cs b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Utils.cs
index 0b31646..3a8266f 100644
--- a/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Utils.cs
+++ b/examples/csharp/src/MessageHeaders/Iggy_SDK.Examples.MessageHeaders.Producer/Utils.cs
@@ -117,15 +117,7 @@
var topicIdentifier = Identifier.Numeric(TOPIC_ID);
logger.LogInformation("Sending messages count: {Count}", messagesPerBatch);
- await client.SendMessagesAsync(
- new MessageSendRequest
- {
- StreamId = streamIdentifier,
- TopicId = topicIdentifier,
- Partitioning = partitioning,
- Messages = messages
- }
- );
+ await client.SendMessagesAsync(streamIdentifier, topicIdentifier, partitioning, messages);
sentBatches++;
logger.LogInformation("Sent messages: {Messages}.", serializableMessages);
@@ -144,14 +136,20 @@
argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
if (argumentName != "--tcp-server-address")
+ {
throw new FormatException(
$"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
);
+ }
+
tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
throw new FormatException(
$"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
);
+ }
+
logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
return tcpServerAddr;
}
diff --git a/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Iggy_SDK.Examples.NewSdk.Consumer.csproj b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Iggy_SDK.Examples.NewSdk.Consumer.csproj
new file mode 100644
index 0000000..598bd51
--- /dev/null
+++ b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Iggy_SDK.Examples.NewSdk.Consumer.csproj
@@ -0,0 +1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2"/>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1"/>
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1"/>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\foreign\csharp\Iggy_SDK\Iggy_SDK.csproj" />
+ <ProjectReference Include="..\..\Iggy_SDK.Examples.Shared\Iggy_SDK.Examples.Shared.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Program.cs b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Program.cs
new file mode 100644
index 0000000..c4c0d98
--- /dev/null
+++ b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Program.cs
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy;
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Consumers;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Extensions;
+using Apache.Iggy.Factory;
+using Apache.Iggy.Kinds;
+using Iggy_SDK.Examples.NewSdk.Consumer;
+using Microsoft.Extensions.Logging;
+
+var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
+var logger = loggerFactory.CreateLogger<Program>();
+
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator()
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
+
+await client.LoginUser("iggy", "iggy");
+
+var consumer = client.CreateConsumerBuilder(Identifier.String("new-sdk-stream"), Identifier.String("new-sdk-topic"),
+ Consumer.Group("new-sdk-consumer-group"))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithConsumerGroup("new-sdk-consumer-group", true, true)
+ .WithBatchSize(20)
+ .WithAutoCommitMode(AutoCommitMode.AfterReceive)
+ .WithLogger(loggerFactory)
+ .OnPollingError((s, e) =>
+ {
+ logger.LogError("Polling error: {Message}", e.Exception.Message);
+ })
+ .Build();
+
+await consumer.InitAsync();
+var cancellationTokenSource = new CancellationTokenSource();
+
+await foreach (var message in consumer.ReceiveAsync().WithCancellation(cancellationTokenSource.Token))
+{
+ Utils.HandleMessage(message, logger);
+ await Task.Delay(200);
+}
diff --git a/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Utils.cs b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Utils.cs
new file mode 100644
index 0000000..bbf58c6
--- /dev/null
+++ b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Consumer/Utils.cs
@@ -0,0 +1,97 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using Apache.Iggy;
+using Apache.Iggy.Consumers;
+using Apache.Iggy.Contracts;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Iggy_SDK.Examples.Shared;
+using Microsoft.Extensions.Logging;
+
+namespace Iggy_SDK.Examples.NewSdk.Consumer;
+
+public static class Utils
+{
+ public static void HandleMessage(ReceivedMessage message, ILogger logger)
+ {
+ var payload = Encoding.UTF8.GetString(message.Message.Payload);
+ var envelope = JsonSerializer.Deserialize<Envelope>(payload) ??
+ throw new Exception("Could not deserialize envelope.");
+
+ logger.LogInformation(
+ "[{Partition}] Handling message type: {MessageType} at offset: {Offset}",
+ message.PartitionId,
+ envelope.MessageType,
+ message.CurrentOffset
+ );
+
+ switch (envelope.MessageType)
+ {
+ case Envelope.OrderCreatedType:
+ var orderCreated = JsonSerializer.Deserialize<OrderCreated>(envelope.Payload) ??
+ throw new Exception("Could not deserialize order_created.");
+ logger.LogInformation("{OrderCreated}", orderCreated);
+ break;
+
+ case Envelope.OrderConfirmedType:
+ var orderConfirmed = JsonSerializer.Deserialize<OrderConfirmed>(envelope.Payload) ??
+ throw new Exception("Could not deserialize order_confirmed.");
+ logger.LogInformation("{OrderConfirmed}", orderConfirmed);
+ break;
+ case Envelope.OrderRejectedType:
+ var orderRejected = JsonSerializer.Deserialize<OrderRejected>(envelope.Payload) ??
+ throw new Exception("Could not deserialize order_rejected.");
+ logger.LogInformation("{OrderRejected}", orderRejected);
+ break;
+ default:
+ logger.LogWarning("Received unknown message type: {MessageType}", envelope.MessageType);
+ break;
+ }
+ }
+
+ public static string GetTcpServerAddr(string[] args, ILogger logger)
+ {
+ var defaultServerAddr = "127.0.0.1:8090";
+ var argumentName = args.Length > 0 ? args[0] : null;
+ var tcpServerAddr = args.Length > 1 ? args[1] : null;
+
+ if (argumentName is null && tcpServerAddr is null) return defaultServerAddr;
+
+ argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
+ if (argumentName != "--tcp-server-address")
+ {
+ throw new FormatException(
+ $"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
+ );
+ }
+
+ tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
+ if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
+ throw new FormatException(
+ $"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
+ );
+ }
+
+ logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
+ return tcpServerAddr;
+ }
+}
diff --git a/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Iggy_SDK.Examples.NewSdk.Producer.csproj b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Iggy_SDK.Examples.NewSdk.Producer.csproj
new file mode 100644
index 0000000..369228b
--- /dev/null
+++ b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Iggy_SDK.Examples.NewSdk.Producer.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net8.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\foreign\csharp\Iggy_SDK\Iggy_SDK.csproj"/>
+ <ProjectReference Include="..\..\Iggy_SDK.Examples.Shared\Iggy_SDK.Examples.Shared.csproj"/>
+ </ItemGroup>
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2"/>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1"/>
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1"/>
+ </ItemGroup>
+</Project>
diff --git a/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Program.cs b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Program.cs
new file mode 100644
index 0000000..aa03c6d
--- /dev/null
+++ b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Program.cs
@@ -0,0 +1,47 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy;
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Extensions;
+using Apache.Iggy.Factory;
+using Iggy_SDK.Examples.NewSdk.Producer;
+using Microsoft.Extensions.Logging;
+
+var loggerFactory = LoggerFactory.Create(b => { b.AddConsole(); });
+ILogger<Program> logger = loggerFactory.CreateLogger<Program>();
+
+var client = IggyClientFactory.CreateClient(new IggyClientConfigurator
+{
+ BaseAddress = Utils.GetTcpServerAddr(args, logger),
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory
+});
+
+await client.LoginUser("iggy", "iggy");
+
+var publisher = client.CreatePublisherBuilder(Identifier.String("new-sdk-stream"), Identifier.String("new-sdk-topic"))
+ .CreateStreamIfNotExists("new-sdk-stream")
+ .CreateTopicIfNotExists("new-sdk-topic", 4)
+ .WithBackgroundSending(batchSize: 5, flushInterval: TimeSpan.FromSeconds(1))
+ .WithLogger(loggerFactory)
+ .Build();
+
+await publisher.InitAsync();
+await Utils.ProduceMessages(publisher, logger);
+await publisher.WaitUntilAllSends();
diff --git a/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Utils.cs b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Utils.cs
new file mode 100644
index 0000000..4884931
--- /dev/null
+++ b/examples/csharp/src/NewSdk/Iggy_SDK.Examples.NewSdk.Producer/Utils.cs
@@ -0,0 +1,110 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Net;
+using System.Text;
+using Apache.Iggy.Messages;
+using Apache.Iggy.Publishers;
+using Iggy_SDK.Examples.Shared;
+using Microsoft.Extensions.Logging;
+
+namespace Iggy_SDK.Examples.NewSdk.Producer;
+
+public static class Utils
+{
+ private const uint BATCHES_LIMIT = 5;
+
+ public static async Task ProduceMessages(IggyPublisher publisher, ILogger logger)
+ {
+ var interval = TimeSpan.FromMilliseconds(500);
+ logger.LogInformation(
+ "Messages will be sent to stream: {StreamId}, topic: {TopicId}} with interval {Interval}.",
+ publisher.StreamId,
+ publisher.TopicId,
+ interval
+ );
+
+ var messagesPerBatch = 10;
+ var sentBatches = 0;
+ var messagesGenerator = new MessagesGenerator();
+
+ while (true)
+ {
+ if (sentBatches == BATCHES_LIMIT)
+ {
+ logger.LogInformation(
+ "Sent {SentBatches} batches of messages, exiting.",
+ sentBatches
+ );
+ return;
+ }
+
+ List<ISerializableMessage> serializableMessages = Enumerable
+ .Range(0, messagesPerBatch)
+ .Aggregate(new List<ISerializableMessage>(), (list, _) =>
+ {
+ var serializableMessage = messagesGenerator.Generate();
+ list.Add(serializableMessage);
+ return list;
+ });
+
+ List<Message> messages = serializableMessages.Select(serializableMessage =>
+ {
+ var jsonEnvelope = serializableMessage.ToJsonEnvelope();
+ return new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes(jsonEnvelope));
+ }
+ ).ToList();
+
+ logger.LogInformation("Sending messages count: {Count}", messagesPerBatch);
+
+ await publisher.SendMessages(messages.ToArray());
+
+ sentBatches++;
+ logger.LogInformation("Sent messages: {Messages}.", serializableMessages);
+
+ await Task.Delay(interval);
+ }
+ }
+
+ public static string GetTcpServerAddr(string[] args, ILogger logger)
+ {
+ var defaultServerAddr = "127.0.0.1:8090";
+ var argumentName = args.Length > 0 ? args[0] : null;
+ var tcpServerAddr = args.Length > 1 ? args[1] : null;
+
+ if (argumentName is null && tcpServerAddr is null) return defaultServerAddr;
+
+ argumentName = argumentName ?? throw new ArgumentNullException(argumentName);
+ if (argumentName != "--tcp-server-address")
+ {
+ throw new FormatException(
+ $"Invalid argument {argumentName}! Usage: --tcp-server-address <server-address>"
+ );
+ }
+
+ tcpServerAddr = tcpServerAddr ?? throw new ArgumentNullException(tcpServerAddr);
+ if (!IPEndPoint.TryParse(tcpServerAddr, out _))
+ {
+ throw new FormatException(
+ $"Invalid server address {tcpServerAddr}! Usage: --tcp-server-address <server-address>"
+ );
+ }
+
+ logger.LogInformation("Using server address: {TcpServerAddr}", tcpServerAddr);
+ return tcpServerAddr;
+ }
+}
diff --git a/foreign/csharp/Benchmarks/Program.cs b/foreign/csharp/Benchmarks/Program.cs
index 172fdbc..7c9cab1 100644
--- a/foreign/csharp/Benchmarks/Program.cs
+++ b/foreign/csharp/Benchmarks/Program.cs
@@ -17,6 +17,7 @@
using Apache.Iggy;
using Apache.Iggy.Benchmarks;
+using Apache.Iggy.Configuration;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
using Apache.Iggy.IggyClient;
@@ -38,27 +39,22 @@
for (var i = 0; i < producerCount; i++)
{
- var bus = MessageStreamFactory.CreateMessageStream(options =>
+ var bus = IggyClientFactory.CreateClient(new IggyClientConfigurator()
{
- options.BaseAdress = "127.0.0.1:8090";
- options.Protocol = Protocol.Tcp;
- options.MessageBatchingSettings = x =>
- {
- x.Enabled = false;
- x.MaxMessagesPerBatch = 1000;
- x.Interval = TimeSpan.Zero;
- };
+ BaseAddress = "127.0.0.1:8090",
+ Protocol = Protocol.Tcp,
+ LoggerFactory = loggerFactory,
#if OS_LINUX
- options.ReceiveBufferSize = Int32.MaxValue;
- options.SendBufferSize = Int32.MaxValue;
+ ReceiveBufferSize = Int32.MaxValue,
+ SendBufferSize = Int32.MaxValue,
#elif OS_WINDOWS
- options.ReceiveBufferSize = int.MaxValue;
- options.SendBufferSize = int.MaxValue;
+ ReceiveBufferSize = int.MaxValue,
+ SendBufferSize = int.MaxValue,
#elif OS_MAC
- options.ReceiveBufferSize = 7280*1024;
- options.SendBufferSize = 7280*1024;
+ ReceiveBufferSize = 7280*1024,
+ SendBufferSize = 7280*1024,
#endif
- }, loggerFactory);
+ });
await bus.LoginUser("iggy", "iggy");
clients[i] = bus;
@@ -106,4 +102,4 @@
catch
{
Console.WriteLine("Failed to delete streams");
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Benchmarks/SendMessage.cs b/foreign/csharp/Benchmarks/SendMessage.cs
index a81d7f8..9cc5b11 100644
--- a/foreign/csharp/Benchmarks/SendMessage.cs
+++ b/foreign/csharp/Benchmarks/SendMessage.cs
@@ -39,13 +39,7 @@
for (var i = 0; i < messagesBatch; i++)
{
var startTime = Stopwatch.GetTimestamp();
- await bus.SendMessagesAsync(new MessageSendRequest
- {
- StreamId = streamId,
- TopicId = topicId,
- Partitioning = Partitioning.PartitionId(1),
- Messages = messages
- });
+ await bus.SendMessagesAsync(streamId, topicId, Partitioning.PartitionId(1), messages);
var diff = Stopwatch.GetElapsedTime(startTime);
latencies.Add(diff);
}
@@ -83,4 +77,4 @@
var payloadString = payloadBuilder.ToString();
return Encoding.UTF8.GetBytes(payloadString);
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/DEPENDENCIES.md b/foreign/csharp/DEPENDENCIES.md
index f39b6a9..2114743 100644
--- a/foreign/csharp/DEPENDENCIES.md
+++ b/foreign/csharp/DEPENDENCIES.md
@@ -5,14 +5,14 @@
FluentAssertions: "6.12.0", "Apache-2.0",
Microsoft.Extensions.Logging: "8.0.1", "MIT",
Microsoft.Extensions.Logging.Console: "8.0.1", "MIT",
-Microsoft.NET.Test.Sdk: "17.14.1", "MIT",
-Microsoft.Testing.Extensions.CodeCoverage: "17.14.2", "MIT",
-Microsoft.Testing.Extensions.TrxReport: "1.8.2", "MIT",
+Microsoft.NET.Test.Sdk: "18.0.0", "MIT",
+Microsoft.Testing.Extensions.CodeCoverage: "18.0.4", "MIT",
+Microsoft.Testing.Extensions.TrxReport: "1.9.0", "MIT",
Moq: "4.20.72", "BSD-3-Clause",
-Reqnroll.xUnit: "3.0.3", "BSD-3-Clause",
+Reqnroll.xUnit: "3.1.2", "BSD-3-Clause",
Shouldly: "4.3.0", "BSD-3-Clause",
System.IO.Hashing: "8.0.0", "MIT",
Testcontainers: "4.7.0", "MIT",
-TUnit: "0.58.3", "MIT",
+TUnit: "0.70.0", "MIT",
xunit: "2.9.3", "Apache-2.0",
-xunit.runner.visualstudio: "3.1.3", "Apache-2.0",
+xunit.runner.visualstudio: "3.1.5", "Apache-2.0",
diff --git a/foreign/csharp/Directory.Packages.props b/foreign/csharp/Directory.Packages.props
index 946b295..c605688 100644
--- a/foreign/csharp/Directory.Packages.props
+++ b/foreign/csharp/Directory.Packages.props
@@ -9,17 +9,17 @@
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
- <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
- <PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="17.14.2" />
- <PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="1.8.4" />
+ <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
+ <PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.0.4" />
+ <PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="1.9.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
- <PackageVersion Include="Reqnroll.xUnit" Version="3.0.3" />
+ <PackageVersion Include="Reqnroll.xUnit" Version="3.1.2" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
<PackageVersion Include="Testcontainers" Version="4.7.0" />
- <PackageVersion Include="TUnit" Version="0.58.3" />
+ <PackageVersion Include="TUnit" Version="0.70.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
- <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4">
+ <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion>
diff --git a/foreign/csharp/Iggy_SDK.Tests.BDD/StepDefinitions/BasicMessagingOperationsSteps.cs b/foreign/csharp/Iggy_SDK.Tests.BDD/StepDefinitions/BasicMessagingOperationsSteps.cs
index 57f7dfd..b13827c 100644
--- a/foreign/csharp/Iggy_SDK.Tests.BDD/StepDefinitions/BasicMessagingOperationsSteps.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.BDD/StepDefinitions/BasicMessagingOperationsSteps.cs
@@ -16,6 +16,7 @@
// // under the License.
using System.Text;
+using Apache.Iggy.Configuration;
using Apache.Iggy.Contracts;
using Apache.Iggy.Enums;
using Apache.Iggy.Factory;
@@ -42,12 +43,11 @@
[Given(@"I have a running Iggy server")]
public async Task GivenIHaveARunningIggyServer()
{
- _context.IggyClient = MessageStreamFactory.CreateMessageStream(configurator =>
+ _context.IggyClient = IggyClientFactory.CreateClient(new IggyClientConfigurator()
{
- configurator.BaseAdress = _context.TcpUrl;
- configurator.Protocol = Protocol.Tcp;
- configurator.MessageBatchingSettings = settings => { settings.Enabled = false; };
- }, NullLoggerFactory.Instance);
+ BaseAddress = _context.TcpUrl,
+ Protocol = Protocol.Tcp
+ });
await _context.IggyClient.PingAsync();
}
@@ -127,17 +127,12 @@
public async Task WhenISendMessagesToStreamTopicPartition(int messageCount, int streamId, int topicId,
int partitionId)
{
- List<Message> messages = Enumerable.Range(1, messageCount)
+ Message[] messages = Enumerable.Range(1, messageCount)
.Select(i => new Message(1, Encoding.UTF8.GetBytes($"Test message {i}")))
- .ToList();
+ .ToArray();
- await _context.IggyClient.SendMessagesAsync(new MessageSendRequest
- {
- StreamId = Identifier.Numeric(streamId),
- TopicId = Identifier.Numeric(topicId),
- Partitioning = Partitioning.PartitionId(partitionId),
- Messages = messages
- });
+ await _context.IggyClient.SendMessagesAsync(Identifier.Numeric(streamId), Identifier.Numeric(topicId),
+ Partitioning.PartitionId(partitionId), messages);
_context.LastSendMessage = messages[^1];
}
@@ -203,9 +198,9 @@
lastPolled.ShouldNotBeNull();
_context.LastSendMessage.ShouldNotBeNull();
- lastPolled.Header.Id.ShouldBe(_context.LastSendMessage.Value.Header.Id);
- lastPolled.Payload.ShouldBe(_context.LastSendMessage.Value.Payload);
+ lastPolled.Header.Id.ShouldBe(_context.LastSendMessage.Header.Id);
+ lastPolled.Payload.ShouldBe(_context.LastSendMessage.Payload);
}
}
-// Test context for sharing data between steps
\ No newline at end of file
+// Test context for sharing data between steps
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/FetchMessagesTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/FetchMessagesTests.cs
index f4d353f..dd82f35 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/FetchMessagesTests.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/FetchMessagesTests.cs
@@ -15,18 +15,14 @@
// // specific language governing permissions and limitations
// // under the License.
-using System.Text;
using Apache.Iggy.Contracts;
using Apache.Iggy.Enums;
using Apache.Iggy.Exceptions;
using Apache.Iggy.Headers;
using Apache.Iggy.Kinds;
-using Apache.Iggy.Messages;
using Apache.Iggy.Tests.Integrations.Fixtures;
using Apache.Iggy.Tests.Integrations.Helpers;
-using Apache.Iggy.Tests.Integrations.Models;
using Shouldly;
-using Partitioning = Apache.Iggy.Kinds.Partitioning;
namespace Apache.Iggy.Tests.Integrations;
@@ -38,59 +34,6 @@
[Test]
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task PollMessagesTMessage_WithNoHeaders_Should_PollMessages_Successfully(Protocol protocol)
- {
- PolledMessages<DummyMessage> response = await Fixture.Clients[protocol].PollMessagesAsync(
- new MessageFetchRequest
- {
- Count = 10,
- AutoCommit = true,
- Consumer = Consumer.New(1),
- PartitionId = 1,
- PollingStrategy = PollingStrategy.Next(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicDummyRequest.Name)
- }, DummyMessage.DeserializeDummyMessage);
-
- response.Messages.Count.ShouldBe(10);
- response.PartitionId.ShouldBe(1);
- response.CurrentOffset.ShouldBe(19u);
- uint offset = 0;
- foreach (MessageResponse<DummyMessage> responseMessage in response.Messages)
- {
- responseMessage.UserHeaders.ShouldBeNull();
- responseMessage.Message.Text.ShouldNotBeNullOrEmpty();
- responseMessage.Message.Text.ShouldContain("Dummy message");
- responseMessage.Header.Checksum.ShouldNotBe(0u);
- responseMessage.Header.Id.ShouldNotBe(0u);
- responseMessage.Header.Offset.ShouldBe(offset++);
- responseMessage.Header.PayloadLength.ShouldNotBe(0);
- responseMessage.Header.UserHeadersLength.ShouldBe(0);
- }
- }
-
- [Test]
- [DependsOn(nameof(PollMessagesTMessage_WithNoHeaders_Should_PollMessages_Successfully))]
- [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task PollMessagesTMessage_Should_Throw_InvalidResponse(Protocol protocol)
- {
- var invalidFetchRequest = new MessageFetchRequest
- {
- Count = 10,
- AutoCommit = true,
- Consumer = Consumer.New(1),
- PartitionId = 1,
- PollingStrategy = PollingStrategy.Next(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.Numeric(2137)
- };
- await Should.ThrowAsync<InvalidResponseException>(() =>
- Fixture.Clients[protocol].PollMessagesAsync(invalidFetchRequest, DummyMessage.DeserializeDummyMessage));
- }
-
- [Test]
- [DependsOn(nameof(PollMessagesTMessage_Should_Throw_InvalidResponse))]
- [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task PollMessages_WithNoHeaders_Should_PollMessages_Successfully(Protocol protocol)
{
var response = await Fixture.Clients[protocol].PollMessagesAsync(new MessageFetchRequest
@@ -119,7 +62,7 @@
[Test]
[DependsOn(nameof(PollMessages_WithNoHeaders_Should_PollMessages_Successfully))]
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task PollMessages_Should_Throw_InvalidResponse(Protocol protocol)
+ public async Task PollMessages_InvalidTopic_Should_Throw_InvalidResponse(Protocol protocol)
{
var invalidFetchRequest = new MessageFetchRequest
{
@@ -137,7 +80,7 @@
}
[Test]
- [DependsOn(nameof(PollMessages_Should_Throw_InvalidResponse))]
+ [DependsOn(nameof(PollMessages_InvalidTopic_Should_Throw_InvalidResponse))]
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task PollMessages_WithHeaders_Should_PollMessages_Successfully(Protocol protocol)
{
@@ -149,7 +92,7 @@
PartitionId = 1,
PollingStrategy = PollingStrategy.Next(),
StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.HeadersTopicRequest.Name)
+ TopicId = Identifier.String(Fixture.TopicHeadersRequest.Name)
};
@@ -165,68 +108,4 @@
responseMessage.UserHeaders[HeaderKey.New("header2")].ToInt32().ShouldBeGreaterThan(0);
}
}
-
- [Test]
- [DependsOn(nameof(PollMessages_WithHeaders_Should_PollMessages_Successfully))]
- [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task PollMessagesTMessage_WithHeaders_Should_PollMessages_Successfully(Protocol protocol)
- {
- var headersMessageFetchRequest = new MessageFetchRequest
- {
- Count = 10,
- AutoCommit = true,
- Consumer = Consumer.New(1),
- PartitionId = 1,
- PollingStrategy = PollingStrategy.Next(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicDummyHeaderRequest.Name)
- };
-
- PolledMessages<DummyMessage> response = await Fixture.Clients[protocol]
- .PollMessagesAsync(headersMessageFetchRequest, DummyMessage.DeserializeDummyMessage);
- response.Messages.Count.ShouldBe(10);
- response.PartitionId.ShouldBe(1);
- response.CurrentOffset.ShouldBe(19u);
- foreach (MessageResponse<DummyMessage> responseMessage in response.Messages)
- {
- responseMessage.UserHeaders.ShouldNotBeNull();
- responseMessage.UserHeaders.Count.ShouldBe(2);
- responseMessage.UserHeaders[HeaderKey.New("header1")].ToString().ShouldBe("value1");
- responseMessage.UserHeaders[HeaderKey.New("header2")].ToInt32().ShouldBeGreaterThan(0);
- }
- }
-
- [Test]
- [DependsOn(nameof(PollMessagesTMessage_WithHeaders_Should_PollMessages_Successfully))]
- [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task PollMessagesMessage_WithEncryptor_Should_PollMessages_Successfully(Protocol protocol)
- {
- await Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- Identifier.String(Fixture.TopicRequest.Name), Partitioning.None(),
- [new Message(Guid.NewGuid(), "Test message"u8.ToArray())], bytes =>
- {
- Array.Reverse(bytes);
- return bytes;
- });
-
- var messageFetchRequest = new MessageFetchRequest
- {
- Count = 1,
- AutoCommit = true,
- Consumer = Consumer.New(1),
- PartitionId = 1,
- PollingStrategy = PollingStrategy.Last(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicRequest.Name)
- };
-
- var response = await Fixture.Clients[protocol].PollMessagesAsync(messageFetchRequest, bytes =>
- {
- Array.Reverse(bytes);
- return bytes;
- }, CancellationToken.None);
-
- response.Messages.Count.ShouldBe(1);
- Encoding.UTF8.GetString(response.Messages[0].Payload).ShouldBe("Test message");
- }
}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FetchMessagesFixture.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FetchMessagesFixture.cs
index bfb3d26..66f94e1 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FetchMessagesFixture.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FetchMessagesFixture.cs
@@ -16,14 +16,12 @@
// // under the License.
using System.Text;
-using Apache.Iggy.Contracts;
using Apache.Iggy.Contracts.Http;
using Apache.Iggy.Enums;
using Apache.Iggy.Headers;
using Apache.Iggy.IggyClient;
using Apache.Iggy.Messages;
using Apache.Iggy.Tests.Integrations.Helpers;
-using Apache.Iggy.Tests.Integrations.Models;
using TUnit.Core.Interfaces;
using Partitioning = Apache.Iggy.Kinds.Partitioning;
@@ -31,11 +29,9 @@
public class FetchMessagesFixture : IAsyncInitializer
{
- internal readonly CreateTopicRequest HeadersTopicRequest = TopicFactory.CreateTopic("HeadersTopic");
internal readonly int MessageCount = 20;
internal readonly string StreamId = "FetchMessagesStream";
- internal readonly CreateTopicRequest TopicDummyHeaderRequest = TopicFactory.CreateTopic("DummyHeaderTopic");
- internal readonly CreateTopicRequest TopicDummyRequest = TopicFactory.CreateTopic("DummyTopic");
+ internal readonly CreateTopicRequest TopicHeadersRequest = TopicFactory.CreateTopic("HeadersTopic");
internal readonly CreateTopicRequest TopicRequest = TopicFactory.CreateTopic("Topic");
[ClassDataSource<IggyServerFixture>(Shared = SharedType.PerAssembly)]
@@ -48,64 +44,23 @@
Clients = await IggyServerFixture.CreateClients();
foreach (KeyValuePair<Protocol, IIggyClient> client in Clients)
{
- var streamId = StreamId.GetWithProtocol(client.Key);
- var request = new MessageSendRequest
- {
- StreamId = Identifier.String(streamId),
- TopicId = Identifier.String(TopicRequest.Name),
- Partitioning = Partitioning.None(),
- Messages = CreateMessagesWithoutHeader(MessageCount)
- };
+ var streamId = Identifier.String(StreamId.GetWithProtocol(client.Key));
- var requestWithHeaders = new MessageSendRequest
- {
- StreamId = Identifier.String(streamId),
- TopicId = Identifier.String(HeadersTopicRequest.Name),
- Partitioning = Partitioning.None(),
- Messages = CreateMessagesWithHeader(MessageCount)
- };
+ await client.Value.CreateStreamAsync(streamId.GetString());
+ await client.Value.CreateTopicAsync(streamId, TopicRequest.Name,
+ TopicRequest.PartitionsCount, topicId: TopicRequest.TopicId);
+ await client.Value.CreateTopicAsync(streamId, TopicHeadersRequest.Name,
+ TopicHeadersRequest.PartitionsCount, topicId: TopicHeadersRequest.TopicId);
- var requestDummyMessage = new MessageSendRequest<DummyMessage>
- {
- StreamId = Identifier.String(streamId),
- TopicId = Identifier.String(TopicDummyRequest.Name),
- Partitioning = Partitioning.None(),
- Messages = CreateDummyMessagesWithoutHeader(MessageCount)
- };
+ await client.Value.SendMessagesAsync(streamId, Identifier.String(TopicRequest.Name), Partitioning.None(),
+ CreateMessagesWithoutHeader(MessageCount));
- var requestDummyMessageWithHeaders = new MessageSendRequest<DummyMessage>
- {
- StreamId = Identifier.String(streamId),
- TopicId = Identifier.String(TopicDummyHeaderRequest.Name),
- Partitioning = Partitioning.None(),
- Messages = CreateDummyMessagesWithoutHeader(MessageCount)
- };
-
- await client.Value.CreateStreamAsync(streamId);
- await client.Value.CreateTopicAsync(Identifier.String(streamId), TopicRequest.Name,
- TopicRequest.PartitionsCount,
- topicId: TopicRequest.TopicId);
- await client.Value.CreateTopicAsync(Identifier.String(streamId), TopicDummyRequest.Name,
- TopicDummyRequest.PartitionsCount, topicId: TopicDummyRequest.TopicId);
- await client.Value.CreateTopicAsync(Identifier.String(streamId), HeadersTopicRequest.Name,
- HeadersTopicRequest.PartitionsCount, topicId: HeadersTopicRequest.TopicId);
- await client.Value.CreateTopicAsync(Identifier.String(streamId), TopicDummyHeaderRequest.Name,
- TopicDummyHeaderRequest.PartitionsCount, topicId: TopicDummyHeaderRequest.TopicId);
-
- await client.Value.SendMessagesAsync(request);
- await client.Value.SendMessagesAsync(requestDummyMessage, message => message.SerializeDummyMessage());
- await client.Value.SendMessagesAsync(requestWithHeaders);
- await client.Value.SendMessagesAsync(requestDummyMessageWithHeaders,
- message => message.SerializeDummyMessage(),
- headers: new Dictionary<HeaderKey, HeaderValue>
- {
- { HeaderKey.New("header1"), HeaderValue.FromString("value1") },
- { HeaderKey.New("header2"), HeaderValue.FromInt32(14) }
- });
+ await client.Value.SendMessagesAsync(streamId, Identifier.String(TopicHeadersRequest.Name),
+ Partitioning.None(), CreateMessagesWithHeader(MessageCount));
}
}
- private static List<Message> CreateMessagesWithoutHeader(int count)
+ private static Message[] CreateMessagesWithoutHeader(int count)
{
var messages = new List<Message>();
for (var i = 0; i < count; i++)
@@ -121,25 +76,10 @@
messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes(dummyJson)));
}
- return messages;
+ return messages.ToArray();
}
- private static List<DummyMessage> CreateDummyMessagesWithoutHeader(int count)
- {
- var messages = new List<DummyMessage>();
- for (var i = 0; i < count; i++)
- {
- messages.Add(new DummyMessage
- {
- Text = $"Dummy message {i}",
- Id = i
- });
- }
-
- return messages;
- }
-
- private static List<Message> CreateMessagesWithHeader(int count)
+ private static Message[] CreateMessagesWithHeader(int count)
{
var messages = new List<Message>();
for (var i = 0; i < count; i++)
@@ -160,6 +100,6 @@
}));
}
- return messages;
+ return messages.ToArray();
}
}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FlushMessageFixture.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FlushMessageFixture.cs
index a758e60..4823010 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FlushMessageFixture.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/FlushMessageFixture.cs
@@ -45,19 +45,15 @@
await client.Value.CreateTopicAsync(Identifier.String(StreamId.GetWithProtocol(client.Key)),
TopicRequest.Name, TopicRequest.PartitionsCount);
- await client.Value.SendMessagesAsync(new MessageSendRequest
+ var messages = new Message[]
{
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(StreamId.GetWithProtocol(client.Key)),
- TopicId = Identifier.String(TopicRequest.Name),
- Messages = new List<Message>
- {
- new(Guid.NewGuid(), "Test message 1"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 2"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 3"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 4"u8.ToArray())
- }
- });
+ new(Guid.NewGuid(), "Test message 1"u8.ToArray()),
+ new(Guid.NewGuid(), "Test message 2"u8.ToArray()),
+ new(Guid.NewGuid(), "Test message 3"u8.ToArray()), new(Guid.NewGuid(), "Test message 4"u8.ToArray())
+ };
+ await client.Value.SendMessagesAsync(Identifier.String(StreamId.GetWithProtocol(client.Key)),
+ Identifier.String(TopicRequest.Name), Partitioning.None(), messages);
+
}
}
}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
index 2393d91..c0b2bfc 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/IggyServerFixture.cs
@@ -21,6 +21,7 @@
using Apache.Iggy.IggyClient;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Images;
using Microsoft.Extensions.Logging.Abstractions;
using TUnit.Core.Interfaces;
using TUnit.Core.Logging;
@@ -35,6 +36,10 @@
.WithOutputConsumer(Consume.RedirectStdoutAndStderrToConsole())
.WithWaitStrategy(Wait.ForUnixContainer().UntilInternalTcpPortIsAvailable(8090))
.WithName($"{Guid.NewGuid()}")
+ .WithEnvironment("IGGY_ROOT_USERNAME", "iggy")
+ .WithEnvironment("IGGY_ROOT_PASSWORD", "iggy")
+ .WithEnvironment("IGGY_TCP_ADDRESS", "0.0.0.0:8090")
+ .WithEnvironment("IGGY_HTTP_ADDRESS", "0.0.0.0:3000")
//.WithEnvironment("IGGY_SYSTEM_LOGGING_LEVEL", "trace")
//.WithEnvironment("RUST_LOG", "trace")
.WithCleanUp(true)
@@ -90,7 +95,6 @@
var client = CreateClient(Protocol.Tcp);
await client.LoginUser(userName, password);
- ;
return client;
}
@@ -125,13 +129,11 @@
: $"http://{_iggyServerHost}:3000";
}
- return MessageStreamFactory.CreateMessageStream(options =>
+ return IggyClientFactory.CreateClient(new IggyClientConfigurator()
{
- options.BaseAdress = address;
- options.Protocol = protocol;
- options.MessageBatchingSettings = BatchingSettings;
- options.MessagePollingSettings = PollingSettings;
- }, NullLoggerFactory.Instance);
+ BaseAddress = address,
+ Protocol = protocol
+ });
}
public static IEnumerable<Func<Protocol>> ProtocolData()
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/OffsetFixtures.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/OffsetFixtures.cs
index d3e652c..0867f97 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/OffsetFixtures.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/OffsetFixtures.cs
@@ -45,19 +45,15 @@
await client.Value.CreateTopicAsync(Identifier.String(StreamId.GetWithProtocol(client.Key)),
TopicRequest.Name, TopicRequest.PartitionsCount);
- await client.Value.SendMessagesAsync(new MessageSendRequest
+ var messages = new Message[]
{
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(StreamId.GetWithProtocol(client.Key)),
- TopicId = Identifier.String(TopicRequest.Name),
- Messages = new List<Message>
- {
- new(Guid.NewGuid(), "Test message 1"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 2"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 3"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 4"u8.ToArray())
- }
- });
+ new(Guid.NewGuid(), "Test message 1"u8.ToArray()),
+ new(Guid.NewGuid(), "Test message 2"u8.ToArray()),
+ new(Guid.NewGuid(), "Test message 3"u8.ToArray()),
+ new(Guid.NewGuid(), "Test message 4"u8.ToArray())
+ };
+ await client.Value.SendMessagesAsync(Identifier.String(StreamId.GetWithProtocol(client.Key)),
+ Identifier.String(TopicRequest.Name), Partitioning.None(), messages);
}
}
}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/PollMessagesFixture.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/PollMessagesFixture.cs
deleted file mode 100644
index cb421c7..0000000
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Fixtures/PollMessagesFixture.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-// // Licensed to the Apache Software Foundation (ASF) under one
-// // or more contributor license agreements. See the NOTICE file
-// // distributed with this work for additional information
-// // regarding copyright ownership. The ASF licenses this file
-// // to you under the Apache License, Version 2.0 (the
-// // "License"); you may not use this file except in compliance
-// // with the License. You may obtain a copy of the License at
-// //
-// // http://www.apache.org/licenses/LICENSE-2.0
-// //
-// // Unless required by applicable law or agreed to in writing,
-// // software distributed under the License is distributed on an
-// // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// // KIND, either express or implied. See the License for the
-// // specific language governing permissions and limitations
-// // under the License.
-
-using Apache.Iggy.Contracts;
-using Apache.Iggy.Contracts.Http;
-using Apache.Iggy.Enums;
-using Apache.Iggy.Headers;
-using Apache.Iggy.IggyClient;
-using Apache.Iggy.Tests.Integrations.Helpers;
-using Apache.Iggy.Tests.Integrations.Models;
-using TUnit.Core.Interfaces;
-using Partitioning = Apache.Iggy.Kinds.Partitioning;
-
-namespace Apache.Iggy.Tests.Integrations.Fixtures;
-
-public class PollMessagesFixture : IAsyncInitializer
-{
- internal readonly int MessageCount = 10;
- internal readonly string StreamId = "PollMessagesStream";
- internal readonly CreateTopicRequest TopicRequest = TopicFactory.CreateTopic("Topic");
-
- [ClassDataSource<IggyServerFixture>(Shared = SharedType.PerAssembly)]
- public required IggyServerFixture IggyServerFixture { get; init; }
-
- public Dictionary<Protocol, IIggyClient> Clients { get; set; } = new();
-
- public async Task InitializeAsync()
- {
- Clients = await IggyServerFixture.CreateClients();
- foreach (KeyValuePair<Protocol, IIggyClient> client in Clients)
- {
- await client.Value.CreateStreamAsync(StreamId.GetWithProtocol(client.Key));
- await client.Value.CreateTopicAsync(Identifier.String(StreamId.GetWithProtocol(client.Key)),
- TopicRequest.Name, TopicRequest.PartitionsCount, topicId: TopicRequest.TopicId);
- await client.Value.SendMessagesAsync(
- new MessageSendRequest<DummyMessage>
- {
- Messages = CreateDummyMessagesWithoutHeader(MessageCount),
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(StreamId.GetWithProtocol(client.Key)),
- TopicId = Identifier.String(TopicRequest.Name)
- },
- message => message.SerializeDummyMessage(),
- headers: new Dictionary<HeaderKey, HeaderValue>
- {
- { HeaderKey.New("header1"), HeaderValue.FromString("value1") },
- { HeaderKey.New("header2"), HeaderValue.FromInt32(14) }
- });
- }
- }
-
- private static List<DummyMessage> CreateDummyMessagesWithoutHeader(int count)
- {
- var messages = new List<DummyMessage>();
- for (var i = 0; i < count; i++)
- {
- messages.Add(new DummyMessage
- {
- Text = $"Dummy message {i}",
- Id = i
- });
- }
-
- return messages;
- }
-}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/IggyConsumerTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/IggyConsumerTests.cs
new file mode 100644
index 0000000..21111d0
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/IggyConsumerTests.cs
@@ -0,0 +1,838 @@
+// // Licensed to the Apache Software Foundation (ASF) under one
+// // or more contributor license agreements. See the NOTICE file
+// // distributed with this work for additional information
+// // regarding copyright ownership. The ASF licenses this file
+// // to you under the Apache License, Version 2.0 (the
+// // "License"); you may not use this file except in compliance
+// // with the License. You may obtain a copy of the License at
+// //
+// // http://www.apache.org/licenses/LICENSE-2.0
+// //
+// // Unless required by applicable law or agreed to in writing,
+// // software distributed under the License is distributed on an
+// // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// // KIND, either express or implied. See the License for the
+// // specific language governing permissions and limitations
+// // under the License.
+
+using System.Text;
+using Apache.Iggy.Consumers;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Exceptions;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Apache.Iggy.Messages;
+using Apache.Iggy.Tests.Integrations.Attributes;
+using Apache.Iggy.Tests.Integrations.Fixtures;
+using Shouldly;
+using Partitioning = Apache.Iggy.Kinds.Partitioning;
+
+namespace Apache.Iggy.Tests.Integrations;
+
+public class IggyConsumerTests
+{
+ [ClassDataSource<IggyServerFixture>(Shared = SharedType.PerAssembly)]
+ public required IggyServerFixture Fixture { get; init; }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithSingleConsumer_Should_Initialize_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(1))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .Build();
+
+ await Should.NotThrowAsync(() => consumer.InitAsync());
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [SkipHttp]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithConsumerGroup_Should_Initialize_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.Group("test-group-init"))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithConsumerGroup("test-group-init")
+ .Build();
+
+ await Should.NotThrowAsync(() => consumer.InitAsync());
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_CalledTwice_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(2))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .Build();
+
+ await consumer.InitAsync();
+ await Should.NotThrowAsync(() => consumer.InitAsync());
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithoutInit_Should_Throw_ConsumerNotInitializedException(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(3))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .Build();
+
+ await Should.ThrowAsync<ConsumerNotInitializedException>(async () =>
+ {
+ IAsyncEnumerator<ReceivedMessage> enumerator = consumer.ReceiveAsync().GetAsyncEnumerator();
+ await enumerator.MoveNextAsync();
+ });
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [SkipHttp]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithConsumerGroup_Should_CreateGroup_WhenNotExists(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var groupName = $"test-group-create-{Guid.NewGuid()}";
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.Group(groupName))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithConsumerGroup(groupName)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var group = await client.GetConsumerGroupByIdAsync(Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Identifier.String(groupName));
+
+ group.ShouldNotBeNull();
+ group.Name.ShouldBe(groupName);
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [SkipHttp]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithConsumerGroup_Should_Throw_WhenGroupNotExists_AndAutoCreateDisabled(
+ Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var groupName = $"nonexistent-group-{Guid.NewGuid()}";
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.Group(groupName))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithConsumerGroup(groupName, false)
+ .Build();
+
+ await Should.ThrowAsync<ConsumerGroupNotFoundException>(() => consumer.InitAsync());
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [SkipHttp]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithConsumerGroup_Should_JoinGroup_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var groupName = $"test-group-join-{Guid.NewGuid()}";
+
+ await client.CreateConsumerGroupAsync(Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ groupName);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.Group(groupName))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithConsumerGroup(groupName, false)
+ .Build();
+
+ await Should.NotThrowAsync(() => consumer.InitAsync());
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [SkipHttp]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_Should_LeaveConsumerGroup(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var groupName = $"test-group-leave-{Guid.NewGuid()}";
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.Group(groupName))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithConsumerGroup(groupName)
+ .Build();
+
+ await consumer.InitAsync();
+ await consumer.DisposeAsync();
+
+ var group = await client.GetConsumerGroupByIdAsync(Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Identifier.String(groupName));
+
+ group.ShouldNotBeNull();
+ group.MembersCount.ShouldBe(0u);
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithSingleConsumer_Should_ReceiveMessages_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(10))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var receivedCount = 0;
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ message.ShouldNotBeNull();
+ message.Message.ShouldNotBeNull();
+ message.Message.Payload.ShouldNotBeNull();
+ message.Status.ShouldBe(MessageStatus.Success);
+ message.PartitionId.ShouldBe(1u);
+
+ receivedCount++;
+ if (receivedCount >= 10)
+ {
+ break;
+ }
+ }
+
+ receivedCount.ShouldBeGreaterThanOrEqualTo(10);
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithBatchSize_Should_RespectBatchSize(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var batchSize = 5u;
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(11))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(batchSize)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var receivedCount = 0;
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ message.ShouldNotBeNull();
+ receivedCount++;
+ if (receivedCount >= batchSize)
+ {
+ break;
+ }
+ }
+
+ receivedCount.ShouldBe((int)batchSize);
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithPollingInterval_Should_RespectInterval(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var pollingInterval = TimeSpan.FromMilliseconds(100);
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(12))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(5)
+ .WithPartitionId(1)
+ .WithPollingInterval(pollingInterval)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var receivedCount = 0;
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ message.ShouldNotBeNull();
+ receivedCount++;
+ if (receivedCount >= 3)
+ {
+ break;
+ }
+ }
+
+ receivedCount.ShouldBeGreaterThanOrEqualTo(3);
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithAutoCommitAfterReceive_Should_StoreOffset(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumerId = protocol == Protocol.Tcp ? 20 : 120;
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(consumerId))
+ .WithPollingStrategy(PollingStrategy.First())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.AfterReceive)
+ .Build();
+
+ await consumer.InitAsync();
+
+ ulong lastOffset = 0;
+ var receivedCount = 0;
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ if (receivedCount >= 5)
+ {
+ break;
+ }
+
+ lastOffset = message.CurrentOffset;
+ receivedCount++;
+ }
+
+ await consumer.DisposeAsync();
+
+ var offset = await client.GetOffsetAsync(Consumer.New(consumerId),
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ 1u);
+
+ offset.ShouldNotBeNull();
+ offset.StoredOffset.ShouldBe(4ul);
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithAutoCommitAfterPoll_Should_StoreOffset(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumerId = protocol == Protocol.Tcp ? 21 : 121;
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(consumerId))
+ .WithPollingStrategy(PollingStrategy.First())
+ .WithBatchSize(5)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.AfterPoll)
+ .Build();
+
+ await consumer.InitAsync();
+
+ ulong lastOffset = 0;
+ var receivedCount = 0;
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ lastOffset = message.CurrentOffset;
+ receivedCount++;
+ if (receivedCount >= 5)
+ {
+ break;
+ }
+ }
+
+ await consumer.DisposeAsync();
+
+ var offset = await client.GetOffsetAsync(Consumer.New(consumerId),
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ 1u);
+
+ offset.ShouldNotBeNull();
+ offset.StoredOffset.ShouldBe(4ul);
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task StoreOffsetAsync_Should_StoreOffset_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumerId = protocol == Protocol.Tcp ? 30 : 130;
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(consumerId))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(5)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var testOffset = 42ul;
+ await Should.NotThrowAsync(() => consumer.StoreOffsetAsync(testOffset, 1));
+
+ var offset = await client.GetOffsetAsync(Consumer.New(consumerId),
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ 1u);
+
+ offset.ShouldNotBeNull();
+ offset.StoredOffset.ShouldBe(testOffset);
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [SkipHttp]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DeleteOffsetAsync_Should_DeleteOffset_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumerId = protocol == Protocol.Tcp ? 31 : 131;
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(consumerId))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(5)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var testOffset = 50ul;
+ await consumer.StoreOffsetAsync(testOffset, 1);
+
+ var offset = await client.GetOffsetAsync(Consumer.New(consumerId),
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ 1u);
+ offset.ShouldNotBeNull();
+
+ await Should.NotThrowAsync(() => consumer.DeleteOffsetAsync(1));
+
+ var deletedOffset = await client.GetOffsetAsync(Consumer.New(consumerId),
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ 1u);
+
+ deletedOffset.ShouldBeNull();
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(40))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .Build();
+
+ await consumer.InitAsync();
+ await Should.NotThrowAsync(async () => await consumer.DisposeAsync());
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_CalledTwice_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(41))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .Build();
+
+ await consumer.InitAsync();
+ await consumer.DisposeAsync();
+ await Should.NotThrowAsync(async () => await consumer.DisposeAsync());
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_WithoutInit_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(42))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(1)
+ .Build();
+
+ await Should.NotThrowAsync(async () => await consumer.DisposeAsync());
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task OnPollingError_Should_Fire_WhenPollingFails(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var errorFired = false;
+ Exception? capturedError = null;
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(50))
+ .WithPollingStrategy(PollingStrategy.Next())
+ .WithBatchSize(10)
+ .WithPartitionId(999)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .OnPollingError((sender, args) =>
+ {
+ errorFired = true;
+ capturedError = args.Exception;
+ })
+ .Build();
+
+ await consumer.InitAsync();
+
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
+
+ try
+ {
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ // Should not receive any messages
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ }
+
+ errorFired.ShouldBeTrue();
+ capturedError.ShouldNotBeNull();
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithOffsetStrategy_Should_StartFromOffset(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var startOffset = 10ul;
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(60))
+ .WithPollingStrategy(PollingStrategy.Offset(startOffset))
+ .WithBatchSize(5)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ var firstMessage = true;
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ if (firstMessage)
+ {
+ message.CurrentOffset.ShouldBeGreaterThanOrEqualTo(startOffset);
+ break;
+ }
+ }
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithFirstStrategy_Should_StartFromBeginning(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(61))
+ .WithPollingStrategy(PollingStrategy.First())
+ .WithBatchSize(1)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ message.CurrentOffset.ShouldBe(0ul);
+ break;
+ }
+
+ await consumer.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task ReceiveAsync_WithLastStrategy_Should_StartFromEnd(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStreamWithMessages(client, protocol);
+
+ var consumer = IggyConsumerBuilder
+ .Create(client,
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ Consumer.New(62))
+ .WithPollingStrategy(PollingStrategy.Last())
+ .WithBatchSize(1)
+ .WithPartitionId(1)
+ .WithAutoCommitMode(AutoCommitMode.Disabled)
+ .Build();
+
+ await consumer.InitAsync();
+
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+ var messageReceived = false;
+
+ await foreach (var message in consumer.ReceiveAsync(cts.Token))
+ {
+ message.CurrentOffset.ShouldBeGreaterThan(0ul);
+ messageReceived = true;
+ break;
+ }
+
+ messageReceived.ShouldBeTrue();
+ await consumer.DisposeAsync();
+ }
+
+ private async Task<TestStreamInfo> CreateTestStreamWithMessages(IIggyClient client, Protocol protocol,
+ uint partitionsCount = 5, int messagesPerPartition = 100)
+ {
+ var streamId = $"stream_{Guid.NewGuid()}_{protocol.ToString().ToLowerInvariant()}";
+ var topicId = "test_topic";
+
+ await client.CreateStreamAsync(streamId);
+ await client.CreateTopicAsync(Identifier.String(streamId), topicId, partitionsCount);
+
+ for (uint partitionId = 1; partitionId <= partitionsCount; partitionId++)
+ {
+ var messages = new List<Message>();
+ for (var i = 0; i < messagesPerPartition; i++)
+ {
+ var message = new Message(Guid.NewGuid(),
+ Encoding.UTF8.GetBytes($"Test message {i} for partition {partitionId}"));
+ messages.Add(message);
+ }
+
+ await client.SendMessagesAsync(Identifier.String(streamId),
+ Identifier.String(topicId),
+ Partitioning.PartitionId((int)partitionId),
+ messages);
+ }
+
+ return new TestStreamInfo(streamId, topicId, partitionsCount, messagesPerPartition);
+ }
+
+ private record TestStreamInfo(string StreamId, string TopicId, uint PartitionsCount, int MessagesPerPartition);
+}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/IggyPublisherTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/IggyPublisherTests.cs
new file mode 100644
index 0000000..bc68e5a
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/IggyPublisherTests.cs
@@ -0,0 +1,606 @@
+// // Licensed to the Apache Software Foundation (ASF) under one
+// // or more contributor license agreements. See the NOTICE file
+// // distributed with this work for additional information
+// // regarding copyright ownership. The ASF licenses this file
+// // to you under the Apache License, Version 2.0 (the
+// // "License"); you may not use this file except in compliance
+// // with the License. You may obtain a copy of the License at
+// //
+// // http://www.apache.org/licenses/LICENSE-2.0
+// //
+// // Unless required by applicable law or agreed to in writing,
+// // software distributed under the License is distributed on an
+// // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// // KIND, either express or implied. See the License for the
+// // specific language governing permissions and limitations
+// // under the License.
+
+using System.Text;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Exceptions;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Apache.Iggy.Messages;
+using Apache.Iggy.Publishers;
+using Apache.Iggy.Tests.Integrations.Fixtures;
+using Shouldly;
+using Partitioning = Apache.Iggy.Kinds.Partitioning;
+
+namespace Apache.Iggy.Tests.Integrations;
+
+public class IggyPublisherTests
+{
+ [ClassDataSource<IggyServerFixture>(Shared = SharedType.PerAssembly)]
+ public required IggyServerFixture Fixture { get; init; }
+
+ private async Task<TestStreamInfo> CreateTestStream(IIggyClient client, Protocol protocol, uint partitionsCount = 5)
+ {
+ var streamId = $"stream_{Guid.NewGuid()}_{protocol.ToString().ToLowerInvariant()}";
+ var topicId = "test_topic";
+
+ await client.CreateStreamAsync(streamId);
+ await client.CreateTopicAsync(Identifier.String(streamId), topicId, partitionsCount);
+
+ return new TestStreamInfo(streamId, topicId, partitionsCount);
+ }
+
+ private record TestStreamInfo(string StreamId, string TopicId, uint PartitionsCount);
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_Should_Initialize_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await Should.NotThrowAsync(() => publisher.InitAsync());
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_CalledTwice_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+ await Should.NotThrowAsync(() => publisher.InitAsync());
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_WithoutInit_Should_Throw_PublisherNotInitializedException(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ var messages = new List<Message>
+ {
+ new(Guid.NewGuid(), Encoding.UTF8.GetBytes("Test message"))
+ };
+
+ await Should.ThrowAsync<PublisherNotInitializedException>(() => publisher.SendMessages(messages));
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_Should_SendMessages_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+
+ var messages = new List<Message>();
+ for (int i = 0; i < 10; i++)
+ {
+ messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes($"Test message {i}")));
+ }
+
+ await Should.NotThrowAsync(() => publisher.SendMessages(messages));
+
+ // Verify messages were sent by polling them
+ var polledMessages = await client.PollMessagesAsync(
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ null,
+ Consumer.New(1),
+ PollingStrategy.Next(),
+ 10,
+ false);
+
+ polledMessages.Messages.Count.ShouldBeGreaterThanOrEqualTo(10);
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_WithEmptyList_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+
+ var emptyMessages = new List<Message>();
+ await Should.NotThrowAsync(() => publisher.SendMessages(emptyMessages));
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithStreamAutoCreate_Should_CreateStream(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var streamId = $"auto_stream_{Guid.NewGuid()}_{protocol.ToString().ToLowerInvariant()}";
+ var topicId = "auto_topic";
+
+ // First create topic manually to test only stream creation
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(streamId), Identifier.String(topicId))
+ .CreateStreamIfNotExists(streamId)
+ .CreateTopicIfNotExists(topicId, 3)
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+
+ // Verify stream was created
+ var stream = await client.GetStreamByIdAsync(Identifier.String(streamId));
+ stream.ShouldNotBeNull();
+ stream.Name.ShouldBe(streamId);
+
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithTopicAutoCreate_Should_CreateTopic(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var streamId = $"stream_{Guid.NewGuid()}_{protocol.ToString().ToLowerInvariant()}";
+ var topicId = "auto_topic";
+
+ // Create stream first
+ await client.CreateStreamAsync(streamId);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(streamId), Identifier.String(topicId))
+ .CreateTopicIfNotExists(topicId, 3)
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+
+ // Verify topic was created
+ var topic = await client.GetTopicByIdAsync(Identifier.String(streamId), Identifier.String(topicId));
+ topic.ShouldNotBeNull();
+ topic.Name.ShouldBe(topicId);
+ topic.PartitionsCount.ShouldBe(3u);
+
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithoutAutoCreate_Should_Throw_WhenStreamNotExists(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var streamId = $"nonexistent_stream_{Guid.NewGuid()}";
+ var topicId = "test_topic";
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(streamId), Identifier.String(topicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await Should.ThrowAsync<StreamNotFoundException>(() => publisher.InitAsync());
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task InitAsync_WithoutAutoCreate_Should_Throw_WhenTopicNotExists(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var streamId = $"stream_{Guid.NewGuid()}_{protocol.ToString().ToLowerInvariant()}";
+ var topicId = "nonexistent_topic";
+
+ // Create stream but not topic
+ await client.CreateStreamAsync(streamId);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(streamId), Identifier.String(topicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await Should.ThrowAsync<TopicNotFoundException>(() => publisher.InitAsync());
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_WithBackgroundSending_Should_SendMessages_Successfully(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .WithBackgroundSending(true, queueCapacity: 1000, batchSize: 10, flushInterval: TimeSpan.FromMilliseconds(50))
+ .Build();
+
+ await publisher.InitAsync();
+
+ var messages = new List<Message>();
+ for (int i = 0; i < 50; i++)
+ {
+ messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes($"Background message {i}")));
+ }
+
+ await publisher.SendMessages(messages);
+ await publisher.WaitUntilAllSends();
+
+ // Verify messages were sent
+ var polledMessages = await client.PollMessagesAsync(
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ null,
+ Consumer.New(2),
+ PollingStrategy.Next(),
+ 50,
+ false);
+
+ polledMessages.Messages.Count.ShouldBeGreaterThanOrEqualTo(50);
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task WaitUntilAllSends_Should_WaitForPendingMessages(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .WithBackgroundSending(true, queueCapacity: 1000, batchSize: 10, flushInterval: TimeSpan.FromMilliseconds(100))
+ .Build();
+
+ await publisher.InitAsync();
+
+ var messages = new List<Message>();
+ for (int i = 0; i < 20; i++)
+ {
+ messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes($"Wait test message {i}")));
+ }
+
+ await publisher.SendMessages(messages);
+
+ // Should not return immediately
+ var startTime = DateTime.UtcNow;
+ await publisher.WaitUntilAllSends();
+ var elapsed = DateTime.UtcNow - startTime;
+
+ // Should have taken some time to flush
+ elapsed.ShouldBeGreaterThan(TimeSpan.Zero);
+
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task WaitUntilAllSends_WithoutBackgroundSending_Should_ReturnImmediately(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .WithBackgroundSending(false)
+ .Build();
+
+ await publisher.InitAsync();
+
+ // Should return immediately without background sending
+ await Should.NotThrowAsync(() => publisher.WaitUntilAllSends());
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_ToMultiplePartitions_Should_DistributeMessages(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol, partitionsCount: 5);
+
+ // Send messages to different partitions
+ for (uint partitionId = 1; partitionId <= 5; partitionId++)
+ {
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId((int)partitionId))
+ .Build();
+
+ await publisher.InitAsync();
+
+ var messages = new List<Message>();
+ for (int i = 0; i < 10; i++)
+ {
+ messages.Add(new Message(Guid.NewGuid(),
+ Encoding.UTF8.GetBytes($"Partition {partitionId} message {i}")));
+ }
+
+ await publisher.SendMessages(messages);
+ await publisher.DisposeAsync();
+ }
+
+ // Verify messages are in different partitions
+ for (uint partitionId = 1; partitionId <= 5; partitionId++)
+ {
+ var polledMessages = await client.PollMessagesAsync(
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ partitionId,
+ Consumer.New((int)partitionId + 10),
+ PollingStrategy.Next(),
+ 10,
+ false);
+
+ polledMessages.Messages.Count.ShouldBeGreaterThanOrEqualTo(10);
+ polledMessages.PartitionId.ShouldBe((int)partitionId);
+ }
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_WithBalancedPartitioning_Should_DistributeAcrossPartitions(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol, partitionsCount: 3);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.None())
+ .Build();
+
+ await publisher.InitAsync();
+
+ var messages = new List<Message>();
+ for (int i = 0; i < 30; i++)
+ {
+ messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes($"Balanced message {i}")));
+ }
+
+ await publisher.SendMessages(messages);
+
+ // Verify messages are distributed across partitions
+ var totalMessages = 0;
+ for (uint partitionId = 1; partitionId <= 3; partitionId++)
+ {
+ var polledMessages = await client.PollMessagesAsync(
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ partitionId,
+ Consumer.New((int)partitionId + 20),
+ PollingStrategy.Next(),
+ 30,
+ false);
+
+ totalMessages += polledMessages.Messages.Count;
+ }
+
+ totalMessages.ShouldBeGreaterThanOrEqualTo(30);
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+ await Should.NotThrowAsync(async () => await publisher.DisposeAsync());
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_CalledTwice_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await publisher.InitAsync();
+ await publisher.DisposeAsync();
+ await Should.NotThrowAsync(async () => await publisher.DisposeAsync());
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DisposeAsync_WithoutInit_Should_NotThrow(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ await Should.NotThrowAsync(async () => await publisher.DisposeAsync());
+ }
+
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task StreamId_Should_ReturnConfiguredStreamId(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ publisher.StreamId.ToString().ShouldBe(testStream.StreamId);
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task TopicId_Should_ReturnConfiguredTopicId(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .Build();
+
+ publisher.TopicId.ToString().ShouldBe(testStream.TopicId);
+ await publisher.DisposeAsync();
+ }
+
+ [Test]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task SendMessages_LargeMessageCount_Should_HandleCorrectly(Protocol protocol)
+ {
+ var client = protocol == Protocol.Tcp
+ ? await Fixture.CreateTcpClient()
+ : await Fixture.CreateHttpClient();
+
+ var testStream = await CreateTestStream(client, protocol);
+
+ var publisher = IggyPublisherBuilder
+ .Create(client, Identifier.String(testStream.StreamId), Identifier.String(testStream.TopicId))
+ .WithPartitioning(Partitioning.PartitionId(1))
+ .WithBackgroundSending(true, queueCapacity: 1000, batchSize: 50)
+ .Build();
+
+ await publisher.InitAsync();
+
+ var messages = new List<Message>();
+ for (int i = 0; i < 200; i++)
+ {
+ messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes($"Large batch message {i}")));
+ }
+
+ await publisher.SendMessages(messages);
+ await publisher.WaitUntilAllSends();
+
+ // Verify at least most messages were sent
+ var polledMessages = await client.PollMessagesAsync(
+ Identifier.String(testStream.StreamId),
+ Identifier.String(testStream.TopicId),
+ null,
+ Consumer.New(40),
+ PollingStrategy.Next(),
+ 200,
+ false);
+
+ polledMessages.Messages.Count.ShouldBeGreaterThanOrEqualTo(200);
+ await publisher.DisposeAsync();
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj b/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
index 1fc2928..2eb6524 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/Iggy_SDK.Tests.Integration.csproj
@@ -26,4 +26,4 @@
<ProjectReference Include="..\Iggy_SDK\Iggy_SDK.csproj" />
</ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/OffsetTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/OffsetTests.cs
index 9a22aed..7898f5b 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/OffsetTests.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/OffsetTests.cs
@@ -17,6 +17,7 @@
using Apache.Iggy.Enums;
using Apache.Iggy.Kinds;
+using Apache.Iggy.Tests.Integrations.Attributes;
using Apache.Iggy.Tests.Integrations.Fixtures;
using Apache.Iggy.Tests.Integrations.Helpers;
using Shouldly;
@@ -97,4 +98,21 @@
offset.PartitionId.ShouldBe(1);
offset.CurrentOffset.ShouldBe(3u);
}
+
+ [Test]
+ [SkipHttp]
+ [DependsOn(nameof(GetOffset_ConsumerGroup_ByName_Should_GetOffset_Successfully))]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task DeleteOffset_ConsumerGroup_Should_DeleteOffset_Successfully(Protocol protocol)
+ {
+ await Fixture.Clients[protocol].DeleteOffsetAsync(Consumer.Group("test_consumer_group"),
+ Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)), Identifier.String(Fixture.TopicRequest.Name),
+ 1);
+
+ var offset = await Fixture.Clients[protocol].GetOffsetAsync(Consumer.Group("test_consumer_group"),
+ Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)), Identifier.String(Fixture.TopicRequest.Name),
+ 1);
+
+ offset.ShouldBeNull();
+ }
}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/PollMessagesTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/PollMessagesTests.cs
deleted file mode 100644
index c937228..0000000
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/PollMessagesTests.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-// // Licensed to the Apache Software Foundation (ASF) under one
-// // or more contributor license agreements. See the NOTICE file
-// // distributed with this work for additional information
-// // regarding copyright ownership. The ASF licenses this file
-// // to you under the Apache License, Version 2.0 (the
-// // "License"); you may not use this file except in compliance
-// // with the License. You may obtain a copy of the License at
-// //
-// // http://www.apache.org/licenses/LICENSE-2.0
-// //
-// // Unless required by applicable law or agreed to in writing,
-// // software distributed under the License is distributed on an
-// // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// // KIND, either express or implied. See the License for the
-// // specific language governing permissions and limitations
-// // under the License.
-
-using Apache.Iggy.Contracts;
-using Apache.Iggy.Enums;
-using Apache.Iggy.Kinds;
-using Apache.Iggy.Tests.Integrations.Fixtures;
-using Apache.Iggy.Tests.Integrations.Helpers;
-using Apache.Iggy.Tests.Integrations.Models;
-using Shouldly;
-
-namespace Apache.Iggy.Tests.Integrations;
-
-public class PollMessagesTests
-{
- [ClassDataSource<PollMessagesFixture>(Shared = SharedType.PerClass)]
- public required PollMessagesFixture Fixture { get; init; }
-
- [Test]
- [Timeout(60_000)]
- [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task PollMessagesTMessage_Should_PollMessages_Successfully(Protocol protocol, CancellationToken token)
- {
- var messageCount = 0;
- await foreach (MessageResponse<DummyMessage> msgResponse in Fixture.Clients[protocol].PollMessagesAsync(
- new PollMessagesRequest
- {
- Consumer = Consumer.New(1),
- Count = 10,
- PartitionId = 1,
- PollingStrategy = PollingStrategy.Next(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicRequest.Name)
- }, DummyMessage.DeserializeDummyMessage, token: token))
- {
- msgResponse.UserHeaders.ShouldNotBeNull();
- msgResponse.UserHeaders.Count.ShouldBe(2);
- msgResponse.Message.Text.ShouldContain("Dummy message");
- messageCount++;
- if (messageCount == Fixture.MessageCount)
- {
- break;
- }
- }
-
- messageCount.ShouldBe(Fixture.MessageCount);
- }
-}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/SendMessagesTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/SendMessagesTests.cs
index ec6469c..a137cca 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/SendMessagesTests.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/SendMessagesTests.cs
@@ -76,13 +76,9 @@
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task SendMessages_NoHeaders_Should_SendMessages_Successfully(Protocol protocol)
{
- await Should.NotThrowAsync(() => Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- Messages = _messagesWithoutHeaders,
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicRequest.Name)
- }));
+ await Should.NotThrowAsync(() =>
+ Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
+ Identifier.String(Fixture.TopicRequest.Name), Partitioning.None(), _messagesWithoutHeaders));
}
[Test]
@@ -91,13 +87,8 @@
public async Task SendMessages_NoHeaders_Should_Throw_InvalidResponse(Protocol protocol)
{
await Should.ThrowAsync<InvalidResponseException>(() =>
- Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- Messages = _messagesWithoutHeaders,
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.Numeric(69)
- }));
+ Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
+ Identifier.Numeric(69), Partitioning.None(), _messagesWithoutHeaders));
}
[Test]
@@ -105,13 +96,9 @@
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task SendMessages_WithHeaders_Should_SendMessages_Successfully(Protocol protocol)
{
- await Should.NotThrowAsync(() => Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- Messages = _messagesWithHeaders,
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicRequest.Name)
- }));
+ await Should.NotThrowAsync(() =>
+ Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
+ Identifier.String(Fixture.TopicRequest.Name), Partitioning.None(), _messagesWithHeaders));
}
[Test]
@@ -120,42 +107,7 @@
public async Task SendMessages_WithHeaders_Should_Throw_InvalidResponse(Protocol protocol)
{
await Should.ThrowAsync<InvalidResponseException>(() =>
- Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- Messages = _messagesWithHeaders,
- Partitioning = Partitioning.None(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.Numeric(69)
- }));
- }
-
- [Test]
- [DependsOn(nameof(SendMessages_WithHeaders_Should_Throw_InvalidResponse))]
- [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
- public async Task SendMessages_WithEncryptor_Should_SendMessage_Successfully(Protocol protocol)
- {
- await Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- Identifier.String(Fixture.TopicRequest.Name), Partitioning.None(),
- [new Message(Guid.NewGuid(), "Test message"u8.ToArray())], bytes =>
- {
- Array.Reverse(bytes);
- return bytes;
- });
-
- var messageFetchRequest = new MessageFetchRequest
- {
- Count = 1,
- AutoCommit = true,
- Consumer = Consumer.New(1),
- PartitionId = 1,
- PollingStrategy = PollingStrategy.Last(),
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(Fixture.TopicRequest.Name)
- };
-
- var response = await Fixture.Clients[protocol].PollMessagesAsync(messageFetchRequest);
-
- response.Messages.Count.ShouldBe(1);
- Encoding.UTF8.GetString(response.Messages[0].Payload).ShouldBe("egassem tseT");
+ Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
+ Identifier.Numeric(69), Partitioning.None(), _messagesWithHeaders));
}
}
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/StreamsTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/StreamsTests.cs
index b0cf1a4..6daa503 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/StreamsTests.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/StreamsTests.cs
@@ -109,33 +109,22 @@
topicRequest2.Name, topicRequest2.PartitionsCount, messageExpiry: topicRequest2.MessageExpiry,
topicId: topicRequest2.TopicId);
+ await Fixture.Clients[protocol].SendMessagesAsync(Identifier.Numeric(StreamId.GetWithProtocol(protocol)),
+ Identifier.String(topicRequest1.Name), Partitioning.None(),
+ [
+ new Message(Guid.NewGuid(), "Test message 1"u8.ToArray()),
+ new Message(Guid.NewGuid(), "Test message 2"u8.ToArray()),
+ new Message(Guid.NewGuid(), "Test message 3"u8.ToArray())
+ ]);
- await Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- Partitioning = Partitioning.None(),
- StreamId = Identifier.Numeric(StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(topicRequest1.Name),
- Messages = new List<Message>
- {
- new(Guid.NewGuid(), "Test message 1"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 2"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 3"u8.ToArray())
- }
- });
- await Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- Partitioning = Partitioning.None(),
- StreamId = Identifier.Numeric(StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.String(topicRequest2.Name),
- Messages = new List<Message>
- {
- new(Guid.NewGuid(), "Test message 4"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 5"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 6"u8.ToArray()),
- new(Guid.NewGuid(), "Test message 7"u8.ToArray())
- }
- });
+ await Fixture.Clients[protocol].SendMessagesAsync(Identifier.Numeric(StreamId.GetWithProtocol(protocol)),
+ Identifier.String(topicRequest2.Name), Partitioning.None(), [
+ new Message(Guid.NewGuid(), "Test message 4"u8.ToArray()),
+ new Message(Guid.NewGuid(), "Test message 5"u8.ToArray()),
+ new Message(Guid.NewGuid(), "Test message 6"u8.ToArray()),
+ new Message(Guid.NewGuid(), "Test message 7"u8.ToArray())
+ ]);
var response = await Fixture.Clients[protocol]
.GetStreamByIdAsync(Identifier.Numeric(StreamId.GetWithProtocol(protocol)));
@@ -220,8 +209,10 @@
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task GetStreamById_AfterDelete_Should_Throw_InvalidResponse(Protocol protocol)
{
- await Should.ThrowAsync<InvalidResponseException>(() =>
- Fixture.Clients[protocol].GetStreamByIdAsync(Identifier.Numeric(StreamId.GetWithProtocol(protocol))));
+ var stream = await Fixture.Clients[protocol]
+ .GetStreamByIdAsync(Identifier.Numeric(StreamId.GetWithProtocol(protocol)));
+
+ stream.ShouldBeNull();
}
[Test]
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/SystemTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/SystemTests.cs
index 4a2d773..2552cf2 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/SystemTests.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/SystemTests.cs
@@ -127,13 +127,8 @@
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task GetStats_Should_ReturnValidResponse(Protocol protocol)
{
- await Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.Numeric(1),
- Partitioning = Partitioning.None(),
- Messages = [new Message(Guid.NewGuid(), "Test message"u8.ToArray())]
- });
+ await Fixture.Clients[protocol].SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
+ Identifier.Numeric(1), Partitioning.None(), [new Message(Guid.NewGuid(), "Test message"u8.ToArray())]);
await Fixture.Clients[protocol].PollMessagesAsync(new MessageFetchRequest
{
diff --git a/foreign/csharp/Iggy_SDK.Tests.Integration/TopicsTests.cs b/foreign/csharp/Iggy_SDK.Tests.Integration/TopicsTests.cs
index 094d45b..137a260 100644
--- a/foreign/csharp/Iggy_SDK.Tests.Integration/TopicsTests.cs
+++ b/foreign/csharp/Iggy_SDK.Tests.Integration/TopicsTests.cs
@@ -101,6 +101,29 @@
[Test]
[DependsOn(nameof(Get_ExistingTopic_Should_ReturnValidResponse))]
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
+ public async Task Get_ExistingTopic_ByName_Should_ReturnValidResponse(Protocol protocol)
+ {
+ var response = await Fixture.Clients[protocol]
+ .GetTopicByIdAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
+ Identifier.String(TopicRequest.Name));
+
+ response.ShouldNotBeNull();
+ response.Id.ShouldBe(TopicRequest.TopicId!.Value);
+ response.CreatedAt.UtcDateTime.ShouldBe(DateTimeOffset.UtcNow.UtcDateTime, TimeSpan.FromSeconds(10));
+ response.Name.ShouldBe(TopicRequest.Name);
+ response.CompressionAlgorithm.ShouldBe(TopicRequest.CompressionAlgorithm);
+ response.Partitions!.Count().ShouldBe((int)TopicRequest.PartitionsCount);
+ response.MessageExpiry.ShouldBe(TopicRequest.MessageExpiry);
+ response.Size.ShouldBe(0u);
+ response.PartitionsCount.ShouldBe(TopicRequest.PartitionsCount);
+ response.ReplicationFactor.ShouldBe(TopicRequest.ReplicationFactor);
+ response.MaxTopicSize.ShouldBe(TopicRequest.MaxTopicSize);
+ response.MessagesCount.ShouldBe(0u);
+ }
+
+ [Test]
+ [DependsOn(nameof(Get_ExistingTopic_ByName_Should_ReturnValidResponse))]
+ [MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task Get_ExistingTopics_Should_ReturnValidResponse(Protocol protocol)
{
await Fixture.Clients[protocol].CreateTopicAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
@@ -154,13 +177,9 @@
for (var i = 0; i < 3; i++)
{
- await Fixture.Clients[protocol].SendMessagesAsync(new MessageSendRequest
- {
- StreamId = Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- TopicId = Identifier.Numeric(1),
- Partitioning = Partitioning.None(),
- Messages = GetMessages(i + 2)
- });
+ await Fixture.Clients[protocol]
+ .SendMessagesAsync(Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)), Identifier.Numeric(1),
+ Partitioning.None(), GetMessages(i + 2));
}
var response = await Fixture.Clients[protocol]
@@ -259,9 +278,11 @@
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task Get_NonExistingTopic_Should_Throw_InvalidResponse(Protocol protocol)
{
- await Should.ThrowAsync<InvalidResponseException>(Fixture.Clients[protocol].GetTopicByIdAsync(
+ var topic = await Fixture.Clients[protocol].GetTopicByIdAsync(
Identifier.String(Fixture.StreamId.GetWithProtocol(protocol)),
- Identifier.Numeric(TopicRequest.TopicId!.Value)));
+ Identifier.Numeric(TopicRequest.TopicId!.Value));
+
+ topic.ShouldBeNull();
}
[Test]
@@ -299,14 +320,14 @@
response.MessagesCount.ShouldBe(0u);
}
- private static List<Message> GetMessages(int count)
+ private static Message[] GetMessages(int count)
{
- var messages = new List<Message>();
+ var messages = new List<Message>(count);
for (var i = 0; i < count; i++)
{
messages.Add(new Message(Guid.NewGuid(), Encoding.UTF8.GetBytes($"Test message {i + 1}")));
}
- return messages;
+ return messages.ToArray();
}
}
diff --git a/foreign/csharp/Iggy_SDK/Configuration/IMessageStreamConfigurator.cs b/foreign/csharp/Iggy_SDK/Configuration/IggyClientConfigurator.cs
similarity index 63%
rename from foreign/csharp/Iggy_SDK/Configuration/IMessageStreamConfigurator.cs
rename to foreign/csharp/Iggy_SDK/Configuration/IggyClientConfigurator.cs
index b32d48a..8255fd4 100644
--- a/foreign/csharp/Iggy_SDK/Configuration/IMessageStreamConfigurator.cs
+++ b/foreign/csharp/Iggy_SDK/Configuration/IggyClientConfigurator.cs
@@ -16,16 +16,17 @@
// under the License.
using Apache.Iggy.Enums;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
namespace Apache.Iggy.Configuration;
-public interface IMessageStreamConfigurator
+public sealed class IggyClientConfigurator
{
- string BaseAdress { get; set; }
- Protocol Protocol { get; set; }
- Action<MessageBatchingSettings> MessageBatchingSettings { get; set; }
- Action<MessagePollingSettings> MessagePollingSettings { get; set; }
- Action<TlsSettings> TlsSettings { get; set; }
- int ReceiveBufferSize { get; set; }
- int SendBufferSize { get; set; }
-}
\ No newline at end of file
+ public string BaseAddress { get; set; } = "http://127.0.0.1:3000";
+ public Protocol Protocol { get; set; } = Protocol.Http;
+ public int ReceiveBufferSize { get; set; } = 4096;
+ public int SendBufferSize { get; set; } = 4096;
+ public TlsSettings TlsSettings { get; set; } = new();
+ public ILoggerFactory LoggerFactory { get; set; } = NullLoggerFactory.Instance;
+}
diff --git a/foreign/csharp/Iggy_SDK/Configuration/MessageStreamConfigurator.cs b/foreign/csharp/Iggy_SDK/Configuration/MessageStreamConfigurator.cs
deleted file mode 100644
index 8c06a2d..0000000
--- a/foreign/csharp/Iggy_SDK/Configuration/MessageStreamConfigurator.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using Apache.Iggy.Enums;
-
-namespace Apache.Iggy.Configuration;
-
-public sealed class MessageStreamConfigurator : IMessageStreamConfigurator
-{
- public string BaseAdress { get; set; } = "http://127.0.0.1:3000";
- public Protocol Protocol { get; set; } = Protocol.Http;
-
- public Action<MessagePollingSettings> MessagePollingSettings { get; set; } = options =>
- {
- options.Interval = TimeSpan.FromMilliseconds(100);
- options.StoreOffsetStrategy = StoreOffset.WhenMessagesAreReceived;
- };
-
- public Action<TlsSettings> TlsSettings { get; set; } = options =>
- {
- options.Enabled = false;
- options.Hostname = default!;
- options.Authenticate = false;
- };
-
- public Action<MessageBatchingSettings> MessageBatchingSettings { get; set; } = options =>
- {
- options.Interval = TimeSpan.FromMilliseconds(100);
- options.MaxMessagesPerBatch = 1000;
- options.MaxRequests = 4096;
- };
-
- public int ReceiveBufferSize { get; set; } = 4096;
- public int SendBufferSize { get; set; } = 4096;
-}
\ No newline at end of file
diff --git a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs b/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
index 7e2f473..8c612b8 100644
--- a/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
+++ b/foreign/csharp/Iggy_SDK/Configuration/TlsConfiguration.cs
@@ -21,5 +21,5 @@
{
public bool Enabled { get; set; }
public string Hostname { get; set; } = string.Empty;
- public bool Authenticate { get; set; } = false;
-}
\ No newline at end of file
+ public bool Authenticate { get; set; }
+}
diff --git a/foreign/csharp/Iggy_SDK/ConnectionStream/TcpConnectionStream.cs b/foreign/csharp/Iggy_SDK/ConnectionStream/TcpConnectionStream.cs
index 777627f..07ff5be 100644
--- a/foreign/csharp/Iggy_SDK/ConnectionStream/TcpConnectionStream.cs
+++ b/foreign/csharp/Iggy_SDK/ConnectionStream/TcpConnectionStream.cs
@@ -15,15 +15,13 @@
// specific language governing permissions and limitations
// under the License.
-using System.Net.Sockets;
-
namespace Apache.Iggy.ConnectionStream;
public sealed class TcpConnectionStream : IConnectionStream
{
- private readonly NetworkStream _stream;
+ private readonly Stream _stream;
- public TcpConnectionStream(NetworkStream stream)
+ public TcpConnectionStream(Stream stream)
{
_stream = stream;
}
@@ -52,4 +50,4 @@
{
_stream.Dispose();
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs b/foreign/csharp/Iggy_SDK/Consumers/AutoCommitMode.cs
similarity index 60%
copy from foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
copy to foreign/csharp/Iggy_SDK/Consumers/AutoCommitMode.cs
index f33f1d3..9b06fd0 100644
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
+++ b/foreign/csharp/Iggy_SDK/Consumers/AutoCommitMode.cs
@@ -15,12 +15,30 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
+namespace Apache.Iggy.Consumers;
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal interface IMessageInvoker
+/// <summary>
+/// Auto commit modes
+/// </summary>
+public enum AutoCommitMode
{
- internal Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default);
-}
\ No newline at end of file
+ /// <summary>
+ /// Set auto commit to true on polling messages
+ /// </summary>
+ Auto,
+
+ /// <summary>
+ /// Set offset after polling messages
+ /// </summary>
+ AfterPoll,
+
+ /// <summary>
+ /// Set offset after receive message
+ /// </summary>
+ AfterReceive,
+
+ /// <summary>
+ /// Offset will not be stored automatically
+ /// </summary>
+ Disabled
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/ConsumerErrorEventArgs.cs b/foreign/csharp/Iggy_SDK/Consumers/ConsumerErrorEventArgs.cs
new file mode 100644
index 0000000..1442950
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/ConsumerErrorEventArgs.cs
@@ -0,0 +1,45 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Event arguments for consumer polling errors
+/// </summary>
+public class ConsumerErrorEventArgs : EventArgs
+{
+ /// <summary>
+ /// The exception that occurred during polling
+ /// </summary>
+ public Exception Exception { get; }
+
+ /// <summary>
+ /// A descriptive message about the error
+ /// </summary>
+ public string Message { get; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ConsumerErrorEventArgs" /> class
+ /// </summary>
+ /// <param name="exception">The exception that occurred</param>
+ /// <param name="message">A descriptive error message</param>
+ public ConsumerErrorEventArgs(Exception exception, string message)
+ {
+ Exception = exception;
+ Message = message;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IDeserializer.cs b/foreign/csharp/Iggy_SDK/Consumers/IDeserializer.cs
new file mode 100644
index 0000000..4a82d11
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IDeserializer.cs
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Interface for deserializing message payloads from byte arrays to type T.
+/// <para>
+/// No type constraints are enforced on T to provide maximum flexibility.
+/// Implementations are responsible for ensuring that the provided byte data can be properly deserialized to the target type.
+/// </para>
+/// </summary>
+/// <typeparam name="T">
+/// The target type for deserialization. Can be any type - reference or value type, nullable or non-nullable.
+/// The deserializer implementation must be able to produce instances of the specific type.
+/// </typeparam>
+/// <remarks>
+/// Implementations should throw appropriate exceptions (e.g., <see cref="System.FormatException" />,
+/// <see cref="System.ArgumentException" />, or <see cref="System.InvalidOperationException" />)
+/// if the provided data cannot be deserialized to type T. These exceptions will be caught and logged by
+/// <see cref="IggyConsumer{T}" /> during message processing.
+/// </remarks>
+public interface IDeserializer<out T>
+{
+ /// <summary>
+ /// Deserializes a byte array into an instance of type T.
+ /// </summary>
+ /// <param name="data">The byte array containing the serialized data to deserialize.</param>
+ /// <returns>An instance of type T representing the deserialized data.</returns>
+ /// <exception cref="System.FormatException">
+ /// Thrown when the data format is invalid and cannot be deserialized.
+ /// </exception>
+ /// <exception cref="System.ArgumentException">
+ /// Thrown when the data cannot be deserialized due to invalid content or structure.
+ /// </exception>
+ /// <exception cref="System.InvalidOperationException">
+ /// Thrown when the deserialization operation fails due to state issues.
+ /// </exception>
+ T Deserialize(byte[] data);
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.Logging.cs b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.Logging.cs
new file mode 100644
index 0000000..686575b
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.Logging.cs
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Consumers;
+
+public partial class IggyConsumer
+{
+ // Information logs
+ [LoggerMessage(EventId = 100,
+ Level = LogLevel.Information,
+ Message = "Creating consumer group '{GroupName}' for stream {StreamId}, topic {TopicId}")]
+ private partial void LogCreatingConsumerGroup(string groupName, Identifier streamId, Identifier topicId);
+
+ [LoggerMessage(EventId = 101,
+ Level = LogLevel.Information,
+ Message = "Successfully created consumer group '{GroupName}'")]
+ private partial void LogConsumerGroupCreated(string groupName);
+
+ [LoggerMessage(EventId = 102,
+ Level = LogLevel.Information,
+ Message = "Joining consumer group '{GroupName}' for stream {StreamId}, topic {TopicId}")]
+ private partial void LogJoiningConsumerGroup(string groupName, Identifier streamId, Identifier topicId);
+
+ [LoggerMessage(EventId = 103,
+ Level = LogLevel.Information,
+ Message = "Successfully joined consumer group '{GroupName}'")]
+ private partial void LogConsumerGroupJoined(string groupName);
+
+ // Trace logs
+ [LoggerMessage(EventId = 200,
+ Level = LogLevel.Trace,
+ Message = "Left consumer group '{GroupName}'")]
+ private partial void LogLeftConsumerGroup(string groupName);
+
+ // Warning logs
+ [LoggerMessage(EventId = 302,
+ Level = LogLevel.Warning,
+ Message = "Failed to leave consumer group '{GroupName}'")]
+ private partial void LogFailedToLeaveConsumerGroup(Exception exception, string groupName);
+
+ [LoggerMessage(EventId = 303,
+ Level = LogLevel.Warning,
+ Message = "Failed to logout user or dispose client")]
+ private partial void LogFailedToLogoutOrDispose(Exception exception);
+
+ // Error logs
+ [LoggerMessage(EventId = 400,
+ Level = LogLevel.Error,
+ Message = "Failed to initialize consumer group '{GroupName}'")]
+ private partial void LogFailedToInitializeConsumerGroup(Exception exception, string groupName);
+
+ [LoggerMessage(EventId = 401,
+ Level = LogLevel.Error,
+ Message = "Failed to create consumer group '{GroupName}'")]
+ private partial void LogFailedToCreateConsumerGroup(Exception exception, string groupName);
+
+ [LoggerMessage(EventId = 1,
+ Level = LogLevel.Debug,
+ Message = "Polling task cancelled")]
+ private partial void LogPollingTaskCancelled();
+
+ [LoggerMessage(EventId = 2,
+ Level = LogLevel.Debug,
+ Message = "Message polling stopped")]
+ private partial void LogMessagePollingStopped();
+
+ [LoggerMessage(EventId = 402,
+ Level = LogLevel.Error,
+ Message = "Failed to decrypt message with offset {Offset}")]
+ private partial void LogFailedToDecryptMessage(Exception exception, ulong offset);
+
+ [LoggerMessage(EventId = 403,
+ Level = LogLevel.Error,
+ Message = "Failed to poll messages")]
+ private partial void LogFailedToPollMessages(Exception exception);
+
+ [LoggerMessage(EventId = 300,
+ Level = LogLevel.Warning,
+ Message = "Returned monotonic time went backwards, now < lastPolledAt: ({Now} < {LastPolledAt})")]
+ private partial void LogMonotonicTimeWentBackwards(long now, long lastPolledAt);
+
+ [LoggerMessage(EventId = 201,
+ Level = LogLevel.Trace,
+ Message = "No need to wait before polling messages. {Now} - {LastPolledAt} = {Elapsed}")]
+ private partial void LogNoNeedToWaitBeforePolling(long now, long lastPolledAt, long elapsed);
+
+ [LoggerMessage(EventId = 202,
+ Level = LogLevel.Trace,
+ Message = "Waiting for {Remaining} milliseconds before polling messages")]
+ private partial void LogWaitingBeforePolling(long remaining);
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.cs b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.cs
new file mode 100644
index 0000000..ea3a5a2
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumer.cs
@@ -0,0 +1,401 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using System.Threading.Channels;
+using Apache.Iggy.Contracts;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Exceptions;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// High-level consumer for receiving messages from Iggy streams.
+/// Provides automatic polling, offset management, and consumer group support.
+/// </summary>
+public partial class IggyConsumer : IAsyncDisposable
+{
+ private readonly Channel<ReceivedMessage> _channel;
+ private readonly IIggyClient _client;
+ private readonly IggyConsumerConfig _config;
+ private readonly ConcurrentDictionary<int, ulong> _lastPolledOffset = new();
+ private readonly ILogger<IggyConsumer> _logger;
+ private string? _consumerGroupName;
+ private bool _disposed;
+ private bool _isInitialized;
+ private long _lastPolledAtMs;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IggyConsumer" /> class
+ /// </summary>
+ /// <param name="client">The Iggy client for server communication</param>
+ /// <param name="config">Consumer configuration settings</param>
+ /// <param name="logger">Logger instance for diagnostic output</param>
+ public IggyConsumer(IIggyClient client, IggyConsumerConfig config, ILogger<IggyConsumer> logger)
+ {
+ _client = client;
+ _config = config;
+ _logger = logger;
+
+ _channel = Channel.CreateUnbounded<ReceivedMessage>();
+ }
+
+ /// <summary>
+ /// Disposes the consumer, leaving consumer groups and logging out if applicable
+ /// </summary>
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ if (!string.IsNullOrEmpty(_consumerGroupName) && _isInitialized)
+ {
+ try
+ {
+ await _client.LeaveConsumerGroupAsync(_config.StreamId, _config.TopicId,
+ Identifier.String(_consumerGroupName));
+
+ LogLeftConsumerGroup(_consumerGroupName);
+ }
+ catch (Exception e)
+ {
+ LogFailedToLeaveConsumerGroup(e, _consumerGroupName);
+ }
+ }
+
+ if (_config.CreateIggyClient && _isInitialized)
+ {
+ try
+ {
+ await _client.LogoutUser();
+ _client.Dispose();
+ }
+ catch (Exception e)
+ {
+ LogFailedToLogoutOrDispose(e);
+ }
+ }
+
+ _disposed = true;
+ }
+
+ /// <summary>
+ /// Fired when an error occurs during message polling
+ /// </summary>
+ public event EventHandler<ConsumerErrorEventArgs>? OnPollingError;
+
+ /// <summary>
+ /// Initializes the consumer by logging in (if needed) and setting up consumer groups
+ /// </summary>
+ /// <param name="ct">Cancellation token</param>
+ /// <exception cref="InvalidConsumerGroupNameException">Thrown when consumer group name is invalid</exception>
+ /// <exception cref="ConsumerGroupNotFoundException">Thrown when consumer group doesn't exist and auto-creation is disabled</exception>
+ public async Task InitAsync(CancellationToken ct = default)
+ {
+ if (_isInitialized)
+ {
+ return;
+ }
+
+ if (_config.Consumer.Type == ConsumerType.ConsumerGroup && _config.PartitionId != null)
+ {
+ _logger.LogWarning("PartitionId is ignored when ConsumerType is ConsumerGroup");
+ _config.PartitionId = null;
+ }
+
+ if (_config.CreateIggyClient)
+ {
+ await _client.LoginUser(_config.Login, _config.Password, ct);
+ }
+
+ await InitializeConsumerGroupAsync(ct);
+
+ _isInitialized = true;
+ }
+
+ /// <summary>
+ /// Receives messages asynchronously from the consumer as an async stream.
+ /// Messages are automatically polled from the server and buffered in a bounded channel.
+ /// </summary>
+ /// <param name="ct">Cancellation token to stop receiving messages</param>
+ /// <returns>An async enumerable of received messages</returns>
+ /// <exception cref="ConsumerNotInitializedException">Thrown when InitAsync has not been called</exception>
+ public async IAsyncEnumerable<ReceivedMessage> ReceiveAsync([EnumeratorCancellation] CancellationToken ct = default)
+ {
+ if (!_isInitialized)
+ {
+ throw new ConsumerNotInitializedException();
+ }
+
+ do
+ {
+ if (!_channel.Reader.TryRead(out var message))
+ {
+ await PollMessagesAsync(ct);
+ continue;
+ }
+
+ yield return message;
+
+ if (_config.AutoCommitMode == AutoCommitMode.AfterReceive)
+ {
+ await _client.StoreOffsetAsync(_config.Consumer, _config.StreamId, _config.TopicId,
+ message.CurrentOffset, message.PartitionId, ct);
+ }
+ } while (!ct.IsCancellationRequested);
+ }
+
+ /// <summary>
+ /// Manually stores the consumer offset for a specific partition.
+ /// Use this when auto-commit is disabled or when you need manual offset control.
+ /// </summary>
+ /// <param name="offset">The offset to store</param>
+ /// <param name="partitionId">The partition ID</param>
+ /// <param name="ct">Cancellation token</param>
+ public async Task StoreOffsetAsync(ulong offset, uint partitionId, CancellationToken ct = default)
+ {
+ await _client.StoreOffsetAsync(_config.Consumer, _config.StreamId, _config.TopicId, offset, partitionId, ct);
+ }
+
+ /// <summary>
+ /// Deletes the stored consumer offset for a specific partition.
+ /// The next poll will start from the beginning or based on the polling strategy.
+ /// </summary>
+ /// <param name="partitionId">The partition ID</param>
+ /// <param name="ct">Cancellation token</param>
+ public async Task DeleteOffsetAsync(uint partitionId, CancellationToken ct = default)
+ {
+ await _client.DeleteOffsetAsync(_config.Consumer, _config.StreamId, _config.TopicId, partitionId, ct);
+ }
+
+ /// <summary>
+ /// Initializes consumer group if configured, creating and joining as needed
+ /// </summary>
+ private async Task InitializeConsumerGroupAsync(CancellationToken ct)
+ {
+ if (_config.Consumer.Type == ConsumerType.Consumer)
+ {
+ return;
+ }
+
+ _consumerGroupName = _config.Consumer.Id.Kind == IdKind.String
+ ? _config.Consumer.Id.GetString()
+ : _config.ConsumerGroupName;
+
+ if (string.IsNullOrEmpty(_consumerGroupName))
+ {
+ throw new InvalidConsumerGroupNameException("Consumer group name is empty or null.");
+ }
+
+ try
+ {
+ var existingGroup = await _client.GetConsumerGroupByIdAsync(_config.StreamId, _config.TopicId,
+ Identifier.String(_consumerGroupName), ct);
+
+ if (existingGroup == null && _config.CreateConsumerGroupIfNotExists)
+ {
+ LogCreatingConsumerGroup(_consumerGroupName, _config.StreamId, _config.TopicId);
+
+ var createdGroup = await TryCreateConsumerGroupAsync(_consumerGroupName, _config.Consumer.Id, ct);
+
+ if (createdGroup)
+ {
+ LogConsumerGroupCreated(_consumerGroupName);
+ }
+ }
+ else if (existingGroup == null)
+ {
+ throw new ConsumerGroupNotFoundException(_consumerGroupName);
+ }
+
+ if (_config.JoinConsumerGroup)
+ {
+ LogJoiningConsumerGroup(_consumerGroupName, _config.StreamId, _config.TopicId);
+
+ await _client.JoinConsumerGroupAsync(_config.StreamId, _config.TopicId,
+ Identifier.String(_consumerGroupName), ct);
+
+ LogConsumerGroupJoined(_consumerGroupName);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogFailedToInitializeConsumerGroup(ex, _consumerGroupName);
+ throw;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to create a consumer group, handling the case where it already exists
+ /// </summary>
+ /// <returns>True if the group was created or already exists, false on error</returns>
+ private async Task<bool> TryCreateConsumerGroupAsync(string groupName, Identifier groupId, CancellationToken ct)
+ {
+ try
+ {
+ uint? id = groupId.Kind == IdKind.Numeric ? groupId.GetUInt32() : null;
+ await _client.CreateConsumerGroupAsync(_config.StreamId, _config.TopicId,
+ groupName, id, ct);
+ }
+ catch (IggyInvalidStatusCodeException ex)
+ {
+ // 5004 - Consumer group already exists TODO: refactor errors
+ if (ex.StatusCode != 5004)
+ {
+ LogFailedToCreateConsumerGroup(ex, groupName);
+ return false;
+ }
+
+ return true;
+ }
+
+ return true;
+ }
+
+ /// <summary>
+ /// Polls messages from the server and writes them to the internal channel.
+ /// Handles decryption, offset tracking, and auto-commit logic.
+ /// </summary>
+ private async Task PollMessagesAsync(CancellationToken ct)
+ {
+ try
+ {
+ if (_config.PollingIntervalMs > 0)
+ {
+ await WaitBeforePollingAsync(ct);
+ }
+
+ var messages = await _client.PollMessagesAsync(_config.StreamId, _config.TopicId,
+ _config.PartitionId, _config.Consumer, _config.PollingStrategy, _config.BatchSize,
+ _config.AutoCommit, ct);
+
+ if (_lastPolledOffset.TryGetValue(messages.PartitionId, out var value))
+ {
+ messages.Messages = messages.Messages.Where(x => x.Header.Offset > value).ToList();
+ }
+
+ if (!messages.Messages.Any())
+ {
+ return;
+ }
+
+ foreach (var message in messages.Messages)
+ {
+ var processedMessage = message;
+ var status = MessageStatus.Success;
+ Exception? error = null;
+
+ if (_config.MessageEncryptor != null)
+ {
+ try
+ {
+ var decryptedPayload = _config.MessageEncryptor.Decrypt(message.Payload);
+ processedMessage = new MessageResponse
+ {
+ Header = message.Header,
+ Payload = decryptedPayload,
+ UserHeaders = message.UserHeaders
+ };
+ }
+ catch (Exception ex)
+ {
+ LogFailedToDecryptMessage(ex, message.Header.Offset);
+ status = MessageStatus.DecryptionFailed;
+ error = ex;
+ }
+ }
+
+ var receivedMessage = new ReceivedMessage
+ {
+ Message = processedMessage,
+ CurrentOffset = processedMessage.Header.Offset,
+ PartitionId = (uint)messages.PartitionId,
+ Status = status,
+ Error = error
+ };
+
+ await _channel.Writer.WriteAsync(receivedMessage, ct);
+
+ _lastPolledOffset[messages.PartitionId] = message.Header.Offset;
+ }
+
+ if (_config.AutoCommitMode == AutoCommitMode.AfterPoll)
+ {
+ await _client.StoreOffsetAsync(_config.Consumer, _config.StreamId, _config.TopicId,
+ _lastPolledOffset[messages.PartitionId], (uint)messages.PartitionId, ct);
+ }
+
+ if (_config.PollingStrategy.Kind == MessagePolling.Offset)
+ {
+ _config.PollingStrategy = PollingStrategy.Offset(_lastPolledOffset[messages.PartitionId] + 1);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogFailedToPollMessages(ex);
+ OnPollingError?.Invoke(this, new ConsumerErrorEventArgs(ex, "Failed to poll messages"));
+ }
+ }
+
+ /// <summary>
+ /// Implements polling interval throttling to avoid excessive server requests.
+ /// Uses monotonic time tracking to ensure proper intervals even with clock adjustments.
+ /// </summary>
+ private async Task WaitBeforePollingAsync(CancellationToken ct)
+ {
+ var intervalMs = _config.PollingIntervalMs;
+ if (intervalMs <= 0)
+ {
+ return;
+ }
+
+ var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+ var lastPolledAtMs = Interlocked.Read(ref _lastPolledAtMs);
+
+ if (nowMs < lastPolledAtMs)
+ {
+ LogMonotonicTimeWentBackwards(nowMs, lastPolledAtMs);
+ await Task.Delay(intervalMs, ct);
+ Interlocked.Exchange(ref _lastPolledAtMs, nowMs);
+ return;
+ }
+
+ var elapsedMs = nowMs - lastPolledAtMs;
+ if (elapsedMs >= intervalMs)
+ {
+ LogNoNeedToWaitBeforePolling(nowMs, lastPolledAtMs, elapsedMs);
+ Interlocked.Exchange(ref _lastPolledAtMs, nowMs);
+ return;
+ }
+
+ var remainingMs = intervalMs - elapsedMs;
+ LogWaitingBeforePolling(remainingMs);
+
+ if (remainingMs > 0)
+ {
+ await Task.Delay((int)remainingMs, ct);
+ }
+
+ Interlocked.Exchange(ref _lastPolledAtMs, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerBuilder.cs b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerBuilder.cs
new file mode 100644
index 0000000..9fa720e
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerBuilder.cs
@@ -0,0 +1,316 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Encryption;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Factory;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Builder for creating <see cref="IggyConsumer" /> instances with fluent configuration API
+/// </summary>
+public class IggyConsumerBuilder
+{
+ protected EventHandler<ConsumerErrorEventArgs>? _onPollingError;
+ internal IggyConsumerConfig Config { get; set; } = new();
+ internal IIggyClient? IggyClient { get; set; }
+
+ /// <summary>
+ /// Creates a new consumer builder that will create its own Iggy client.
+ /// You must configure connection settings using <see cref="WithConnection" />.
+ /// </summary>
+ /// <param name="streamId">The stream identifier to consume from</param>
+ /// <param name="topicId">The topic identifier to consume from</param>
+ /// <param name="consumer">Consumer configuration (single consumer or consumer group)</param>
+ /// <returns>A new instance of <see cref="IggyConsumerBuilder" /> to allow method chaining</returns>
+ public static IggyConsumerBuilder Create(Identifier streamId, Identifier topicId, Consumer consumer)
+ {
+ return new IggyConsumerBuilder
+ {
+ Config = new IggyConsumerConfig
+ {
+ CreateIggyClient = true,
+ StreamId = streamId,
+ TopicId = topicId,
+ Consumer = consumer
+ }
+ };
+ }
+
+ /// <summary>
+ /// Creates a new consumer builder using an existing Iggy client.
+ /// Connection settings are not needed as the client is already configured.
+ /// </summary>
+ /// <param name="iggyClient">The existing Iggy client instance to use</param>
+ /// <param name="streamId">The stream identifier to consume from</param>
+ /// <param name="topicId">The topic identifier to consume from</param>
+ /// <param name="consumer">Consumer configuration (single consumer or consumer group)</param>
+ /// <returns>A new instance of <see cref="IggyConsumerBuilder" /> to allow method chaining</returns>
+ public static IggyConsumerBuilder Create(IIggyClient iggyClient, Identifier streamId, Identifier topicId,
+ Consumer consumer)
+ {
+ return new IggyConsumerBuilder
+ {
+ Config = new IggyConsumerConfig
+ {
+ StreamId = streamId,
+ TopicId = topicId,
+ Consumer = consumer
+ },
+ IggyClient = iggyClient
+ };
+ }
+
+ /// <summary>
+ /// Configures the connection settings for the consumer.
+ /// </summary>
+ /// <param name="protocol">The protocol to use for the connection (e.g., TCP, UDP).</param>
+ /// <param name="address">The address of the server to connect to.</param>
+ /// <param name="login">The login username for authentication.</param>
+ /// <param name="password">The password for authentication.</param>
+ /// <param name="receiveBufferSize">The size of the receive buffer.</param>
+ /// <param name="sendBufferSize">The size of the send buffer.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithConnection(Protocol protocol, string address, string login, string password,
+ int receiveBufferSize = 4096, int sendBufferSize = 4096)
+ {
+ Config.Protocol = protocol;
+ Config.Address = address;
+ Config.Login = login;
+ Config.Password = password;
+ Config.ReceiveBufferSize = receiveBufferSize;
+ Config.SendBufferSize = sendBufferSize;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Sets a message decryptor for the consumer, enabling decryption of incoming messages.
+ /// </summary>
+ /// <param name="decryptor">The decryptor implementation to handle message decryption.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithDecryptor(IMessageEncryptor decryptor)
+ {
+ Config.MessageEncryptor = decryptor;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Specifies the partition for the consumer to consume messages from.
+ /// </summary>
+ /// <param name="partitionId">The identifier of the partition to consume from.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithPartitionId(uint partitionId)
+ {
+ Config.PartitionId = partitionId;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Configures the consumer builder with a specified polling strategy.
+ /// A polling strategy defines the starting point for message consumption.
+ /// After first poll, poll strategy is updated to the next message offset.
+ /// </summary>
+ /// <param name="pollingStrategy">A strategy that defines how and from where messages are polled.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithPollingStrategy(PollingStrategy pollingStrategy)
+ {
+ Config.PollingStrategy = pollingStrategy;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the batch size for the consumer. Default is 100.
+ /// </summary>
+ /// <param name="batchSize">The size of the batch to be consumed at one time.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithBatchSize(uint batchSize)
+ {
+ Config.BatchSize = batchSize;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Configures the consumer builder with the specified auto-commit mode.
+ /// </summary>
+ /// <param name="autoCommit">The auto-commit mode to set for the consumer.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithAutoCommitMode(AutoCommitMode autoCommit)
+ {
+ Config.AutoCommit = autoCommit == AutoCommitMode.Auto;
+ Config.AutoCommitMode = autoCommit;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the logger factory for the consumer builder.
+ /// </summary>
+ /// <param name="loggerFactory">The logger factory to be used for logging.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithLogger(ILoggerFactory loggerFactory)
+ {
+ Config.LoggerFactory = loggerFactory;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Configures consumer group settings for the consumer.
+ /// </summary>
+ /// <param name="groupName">The name of the consumer group if consumer kind is numeric</param>
+ /// <param name="createIfNotExists">Whether to create the consumer group if it doesn't exist.</param>
+ /// <param name="joinGroup">Whether to join the consumer group after creation/verification.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder WithConsumerGroup(string groupName, bool createIfNotExists = true, bool joinGroup = true)
+ {
+ Config.ConsumerGroupName = groupName;
+ Config.CreateConsumerGroupIfNotExists = createIfNotExists;
+ Config.JoinConsumerGroup = joinGroup;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Sets the interval between polling attempts to control the rate of server requests
+ /// </summary>
+ /// <param name="interval">The polling interval as a TimeSpan. Set to TimeSpan.Zero to disable throttling.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining</returns>
+ public IggyConsumerBuilder WithPollingInterval(TimeSpan interval)
+ {
+ Config.PollingIntervalMs = (int)interval.TotalMilliseconds;
+ return this;
+ }
+
+ /// <summary>
+ /// Sets an event handler for handling polling errors in the consumer.
+ /// </summary>
+ /// <param name="handler">The event handler to handle polling errors.</param>
+ /// <returns>The current instance of <see cref="IggyConsumerBuilder" /> to allow method chaining.</returns>
+ public IggyConsumerBuilder OnPollingError(EventHandler<ConsumerErrorEventArgs> handler)
+ {
+ _onPollingError = handler;
+ return this;
+ }
+
+
+ /// <summary>
+ /// Builds and returns an instance of <see cref="IggyConsumer" /> configured with the specified options.
+ /// </summary>
+ /// <returns>An instance of <see cref="IggyConsumer" /> based on the current builder configuration.</returns>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid.</exception>
+ public IggyConsumer Build()
+ {
+ Validate();
+
+ if (Config.CreateIggyClient)
+ {
+ IggyClient = IggyClientFactory.CreateClient(new IggyClientConfigurator
+ {
+ Protocol = Config.Protocol,
+ BaseAddress = Config.Address,
+ ReceiveBufferSize = Config.ReceiveBufferSize,
+ SendBufferSize = Config.SendBufferSize
+ });
+ }
+
+ var consumer = new IggyConsumer(IggyClient!, Config,
+ Config.LoggerFactory?.CreateLogger<IggyConsumer>() ??
+ NullLoggerFactory.Instance.CreateLogger<IggyConsumer>());
+
+ if (_onPollingError != null)
+ {
+ consumer.OnPollingError += _onPollingError;
+ }
+
+ return consumer;
+ }
+
+ /// <summary>
+ /// Validates the consumer configuration and throws if invalid.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid.</exception>
+ protected virtual void Validate()
+ {
+ if (Config.CreateIggyClient)
+ {
+ if (string.IsNullOrWhiteSpace(Config.Address))
+ {
+ throw new InvalidOperationException("Address must be provided when CreateIggyClient is true.");
+ }
+
+ if (string.IsNullOrWhiteSpace(Config.Login))
+ {
+ throw new InvalidOperationException("Login must be provided when CreateIggyClient is true.");
+ }
+
+ if (string.IsNullOrWhiteSpace(Config.Password))
+ {
+ throw new InvalidOperationException("Password must be provided when CreateIggyClient is true.");
+ }
+ }
+ else
+ {
+ if (IggyClient == null)
+ {
+ throw new InvalidOperationException(
+ "IggyClient must be provided when CreateIggyClient is false.");
+ }
+ }
+
+ if (Config.ReceiveBufferSize <= 0)
+ {
+ throw new InvalidOperationException("ReceiveBufferSize must be greater than 0.");
+ }
+
+ if (Config.SendBufferSize <= 0)
+ {
+ throw new InvalidOperationException("SendBufferSize must be greater than 0.");
+ }
+
+ if (Config.BatchSize == 0)
+ {
+ throw new InvalidOperationException("BatchSize must be greater than 0.");
+ }
+
+ if (Config.PollingIntervalMs < 0)
+ {
+ throw new InvalidOperationException("PollingIntervalMs cannot be negative.");
+ }
+
+ if (Config.Consumer.Type == ConsumerType.ConsumerGroup)
+ {
+ if (Config.Consumer.Id.Kind == IdKind.Numeric &&
+ Config.CreateConsumerGroupIfNotExists &&
+ string.IsNullOrWhiteSpace(Config.ConsumerGroupName))
+ {
+ throw new InvalidOperationException(
+ "ConsumerGroupName must be provided when using numeric consumer group ID with CreateConsumerGroupIfNotExists enabled.");
+ }
+ }
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerBuilderOfT.cs b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerBuilderOfT.cs
new file mode 100644
index 0000000..d8f5950
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerBuilderOfT.cs
@@ -0,0 +1,141 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Factory;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Builder for creating typed <see cref="IggyConsumer{T}" /> instances with fluent configuration
+/// </summary>
+/// <typeparam name="T">The type to deserialize messages to</typeparam>
+public class IggyConsumerBuilder<T> : IggyConsumerBuilder
+{
+ /// <summary>
+ /// Creates a new typed consumer builder that will create its own Iggy client
+ /// </summary>
+ /// <param name="streamId">The stream identifier to consume from</param>
+ /// <param name="topicId">The topic identifier to consume from</param>
+ /// <param name="consumer">Consumer configuration (single or group)</param>
+ /// <param name="deserializer">The deserializer for converting payloads to type T</param>
+ /// <returns>A new instance of <see cref="IggyConsumerBuilder{T}" /></returns>
+ public static IggyConsumerBuilder<T> Create(Identifier streamId, Identifier topicId, Consumer consumer,
+ IDeserializer<T> deserializer)
+ {
+ return new IggyConsumerBuilder<T>
+ {
+ Config = new IggyConsumerConfig<T>
+ {
+ CreateIggyClient = true,
+ StreamId = streamId,
+ TopicId = topicId,
+ Consumer = consumer,
+ Deserializer = deserializer
+ }
+ };
+ }
+
+ /// <summary>
+ /// Creates a new typed consumer builder using an existing Iggy client
+ /// </summary>
+ /// <param name="iggyClient">The existing Iggy client to use</param>
+ /// <param name="streamId">The stream identifier to consume from</param>
+ /// <param name="topicId">The topic identifier to consume from</param>
+ /// <param name="consumer">Consumer configuration (single or group)</param>
+ /// <param name="deserializer">The deserializer for converting payloads to type T</param>
+ /// <returns>A new instance of <see cref="IggyConsumerBuilder{T}" /></returns>
+ public static IggyConsumerBuilder<T> Create(IIggyClient iggyClient, Identifier streamId, Identifier topicId,
+ Consumer consumer, IDeserializer<T> deserializer)
+ {
+ return new IggyConsumerBuilder<T>
+ {
+ Config = new IggyConsumerConfig<T>
+ {
+ StreamId = streamId,
+ TopicId = topicId,
+ Consumer = consumer,
+ Deserializer = deserializer
+ },
+ IggyClient = iggyClient
+ };
+ }
+
+ /// <summary>
+ /// Builds and returns a typed <see cref="IggyConsumer{T}" /> instance with the configured settings
+ /// </summary>
+ /// <returns>A configured instance of <see cref="IggyConsumer{T}" /></returns>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid</exception>
+ public new IggyConsumer<T> Build()
+ {
+ Validate();
+
+ if (Config.CreateIggyClient)
+ {
+ IggyClient = IggyClientFactory.CreateClient(new IggyClientConfigurator
+ {
+ Protocol = Config.Protocol,
+ BaseAddress = Config.Address,
+ ReceiveBufferSize = Config.ReceiveBufferSize,
+ SendBufferSize = Config.SendBufferSize
+ });
+ }
+
+ if (Config is not IggyConsumerConfig<T> config)
+ {
+ throw new InvalidOperationException("Invalid consumer config");
+ }
+
+ var consumer = new IggyConsumer<T>(IggyClient!, config,
+ Config.LoggerFactory?.CreateLogger<IggyConsumer<T>>() ??
+ NullLoggerFactory.Instance.CreateLogger<IggyConsumer<T>>());
+
+ if (_onPollingError != null)
+ {
+ consumer.OnPollingError += _onPollingError;
+ }
+
+ return consumer;
+ }
+
+ /// <summary>
+ /// Validates the typed consumer configuration, including deserializer validation.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid.</exception>
+ protected override void Validate()
+ {
+ base.Validate();
+
+ if (Config is IggyConsumerConfig<T> typedConfig)
+ {
+ if (typedConfig.Deserializer == null)
+ {
+ throw new InvalidOperationException(
+ $"Deserializer must be provided for typed consumer IggyConsumer<{typeof(T).Name}>.");
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"Config must be of type IggyConsumerConfig<{typeof(T).Name}>.");
+ }
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerConfig.cs b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerConfig.cs
new file mode 100644
index 0000000..250d572
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerConfig.cs
@@ -0,0 +1,152 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Encryption;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Kinds;
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Configuration for a typed Iggy consumer that deserializes messages to type T
+/// </summary>
+/// <typeparam name="T">The type to deserialize messages to</typeparam>
+public class IggyConsumerConfig<T> : IggyConsumerConfig
+{
+ /// <summary>
+ /// Gets or sets the deserializer used to convert message payloads to type T.
+ /// This property is required and cannot be null. The builder will validate that a deserializer
+ /// is provided before creating the consumer instance.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">
+ /// Thrown during consumer build if this property is null.
+ /// </exception>
+ public required IDeserializer<T> Deserializer { get; set; }
+}
+
+/// <summary>
+/// Configuration settings for an Iggy consumer
+/// </summary>
+public class IggyConsumerConfig
+{
+ /// <summary>
+ /// Whether to create a new Iggy client internally. If false, an existing client should be provided.
+ /// </summary>
+ public bool CreateIggyClient { get; set; }
+
+ /// <summary>
+ /// The protocol to use for communication (TCP, QUIC, HTTP)
+ /// </summary>
+ public Protocol Protocol { get; set; }
+
+ /// <summary>
+ /// The server address to connect to
+ /// </summary>
+ public string Address { get; set; } = string.Empty;
+
+ /// <summary>
+ /// The username for authentication
+ /// </summary>
+ public string Login { get; set; } = string.Empty;
+
+ /// <summary>
+ /// The password for authentication
+ /// </summary>
+ public string Password { get; set; } = string.Empty;
+
+ /// <summary>
+ /// The size of the receive buffer in bytes. Default is 4096.
+ /// </summary>
+ public int ReceiveBufferSize { get; set; } = 4096;
+
+ /// <summary>
+ /// The size of the send buffer in bytes. Default is 4096.
+ /// </summary>
+ public int SendBufferSize { get; set; } = 4096;
+
+ /// <summary>
+ /// The identifier of the stream to consume from
+ /// </summary>
+ public Identifier StreamId { get; set; }
+
+ /// <summary>
+ /// The identifier of the topic to consume from
+ /// </summary>
+ public Identifier TopicId { get; set; }
+
+ /// <summary>
+ /// Optional message encryptor for decrypting messages
+ /// </summary>
+ public IMessageEncryptor? MessageEncryptor { get; set; } = null;
+
+ /// <summary>
+ /// Optional partition ID to consume from. If null, consumes from all partitions.
+ /// Note: This is ignored when using consumer groups.
+ /// </summary>
+ public uint? PartitionId { get; set; }
+
+ /// <summary>
+ /// The consumer configuration (single consumer or consumer group)
+ /// </summary>
+ public Consumer Consumer { get; set; }
+
+ /// <summary>
+ /// The polling strategy defining from where to start consuming messages
+ /// </summary>
+ public PollingStrategy PollingStrategy { get; set; }
+
+ /// <summary>
+ /// The maximum number of messages to fetch in a single poll. Default is 100.
+ /// </summary>
+ public uint BatchSize { get; set; } = 100;
+
+ /// <summary>
+ /// Whether to enable automatic offset committing
+ /// </summary>
+ public bool AutoCommit { get; set; }
+
+ /// <summary>
+ /// The auto-commit mode determining when offsets are stored
+ /// </summary>
+ public AutoCommitMode AutoCommitMode { get; set; }
+
+ /// <summary>
+ /// The name of the consumer group (used when consumer ID is numeric)
+ /// </summary>
+ public string? ConsumerGroupName { get; set; }
+
+ /// <summary>
+ /// Whether to create the consumer group if it doesn't exist. Default is true.
+ /// </summary>
+ public bool CreateConsumerGroupIfNotExists { get; set; } = true;
+
+ /// <summary>
+ /// Whether to automatically join the consumer group. Default is true.
+ /// </summary>
+ public bool JoinConsumerGroup { get; set; } = true;
+
+ /// <summary>
+ /// The interval in milliseconds between message polls. Default is 100ms.
+ /// </summary>
+ public int PollingIntervalMs { get; set; } = 100;
+
+ /// <summary>
+ /// Optional logger factory for creating loggers
+ /// </summary>
+ public ILoggerFactory? LoggerFactory { get; set; }
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerOfT.cs b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerOfT.cs
new file mode 100644
index 0000000..c5aeb17
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/IggyConsumerOfT.cs
@@ -0,0 +1,107 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Runtime.CompilerServices;
+using Apache.Iggy.IggyClient;
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Typed consumer that automatically deserializes message payloads to type T.
+/// Extends <see cref="IggyConsumer" /> with deserialization capabilities.
+/// </summary>
+/// <typeparam name="T">The type to deserialize message payloads to</typeparam>
+public class IggyConsumer<T> : IggyConsumer
+{
+ private readonly IggyConsumerConfig<T> _typedConfig;
+ private readonly ILogger<IggyConsumer<T>> _typedLogger;
+
+ /// <summary>
+ /// Initializes a new instance of the typed <see cref="IggyConsumer{T}" /> class
+ /// </summary>
+ /// <param name="client">The Iggy client for server communication</param>
+ /// <param name="config">Typed consumer configuration including deserializer</param>
+ /// <param name="logger">Logger instance for diagnostic output</param>
+ public IggyConsumer(IIggyClient client, IggyConsumerConfig<T> config, ILogger<IggyConsumer<T>> logger) : base(
+ client, config, logger)
+ {
+ _typedConfig = config;
+ _typedLogger = logger;
+ }
+
+ /// <summary>
+ /// Receives and deserializes messages from the consumer
+ /// </summary>
+ /// <param name="ct">Cancellation token</param>
+ /// <returns>Async enumerable of deserialized messages with status</returns>
+ public async IAsyncEnumerable<ReceivedMessage<T>> ReceiveDeserializedAsync(
+ [EnumeratorCancellation] CancellationToken ct = default)
+ {
+ await foreach (var message in ReceiveAsync(ct))
+ {
+ if (message.Status != MessageStatus.Success)
+ {
+ yield return new ReceivedMessage<T>
+ {
+ Data = default,
+ Message = message.Message,
+ CurrentOffset = message.CurrentOffset,
+ PartitionId = message.PartitionId,
+ Status = message.Status,
+ Error = message.Error
+ };
+ continue;
+ }
+
+ T? deserializedPayload = default;
+ Exception? deserializationError = null;
+ var status = MessageStatus.Success;
+
+ try
+ {
+ deserializedPayload = Deserialize(message.Message.Payload);
+ }
+ catch (Exception ex)
+ {
+ _typedLogger.LogError(ex, "Failed to deserialize message at offset {Offset}", message.CurrentOffset);
+ status = MessageStatus.DeserializationFailed;
+ deserializationError = ex;
+ }
+
+ yield return new ReceivedMessage<T>
+ {
+ Data = deserializedPayload,
+ Message = message.Message,
+ CurrentOffset = message.CurrentOffset,
+ PartitionId = message.PartitionId,
+ Status = status,
+ Error = deserializationError
+ };
+ }
+ }
+
+ /// <summary>
+ /// Deserializes a message payload using the configured deserializer
+ /// </summary>
+ /// <param name="payload">The raw byte array payload to deserialize</param>
+ /// <returns>The deserialized object of type T</returns>
+ public T Deserialize(byte[] payload)
+ {
+ return _typedConfig.Deserializer.Deserialize(payload);
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs b/foreign/csharp/Iggy_SDK/Consumers/MessageStatus.cs
similarity index 64%
copy from foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
copy to foreign/csharp/Iggy_SDK/Consumers/MessageStatus.cs
index f33f1d3..bb16b4d 100644
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
+++ b/foreign/csharp/Iggy_SDK/Consumers/MessageStatus.cs
@@ -15,12 +15,25 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
+namespace Apache.Iggy.Consumers;
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal interface IMessageInvoker
+/// <summary>
+/// Represents the status of a received message
+/// </summary>
+public enum MessageStatus
{
- internal Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default);
-}
\ No newline at end of file
+ /// <summary>
+ /// Message was successfully received and processed
+ /// </summary>
+ Success,
+
+ /// <summary>
+ /// Message decryption failed
+ /// </summary>
+ DecryptionFailed,
+
+ /// <summary>
+ /// Message deserialization failed
+ /// </summary>
+ DeserializationFailed
+}
diff --git a/foreign/csharp/Iggy_SDK/Consumers/ReceivedMessage.cs b/foreign/csharp/Iggy_SDK/Consumers/ReceivedMessage.cs
new file mode 100644
index 0000000..9c5160c
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Consumers/ReceivedMessage.cs
@@ -0,0 +1,63 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Contracts;
+
+namespace Apache.Iggy.Consumers;
+
+/// <summary>
+/// Represents a message received from the Iggy consumer with a deserialized payload of type T
+/// </summary>
+/// <typeparam name="T">The type of the deserialized message payload</typeparam>
+public class ReceivedMessage<T> : ReceivedMessage
+{
+ /// <summary>
+ /// The deserialized message payload. Will be null if deserialization failed.
+ /// </summary>
+ public T? Data { get; init; }
+}
+
+/// <summary>
+/// Represents a message received from the Iggy consumer
+/// </summary>
+public class ReceivedMessage
+{
+ /// <summary>
+ /// The underlying message response containing headers, payload, and user headers
+ /// </summary>
+ public required MessageResponse Message { get; init; }
+
+ /// <summary>
+ /// The current offset of this message in the partition
+ /// </summary>
+ public required ulong CurrentOffset { get; init; }
+
+ /// <summary>
+ /// The partition ID from which this message was consumed
+ /// </summary>
+ public uint PartitionId { get; init; }
+
+ /// <summary>
+ /// The status of the message (Success, DecryptionFailed, DeserializationFailed)
+ /// </summary>
+ public MessageStatus Status { get; init; } = MessageStatus.Success;
+
+ /// <summary>
+ /// The exception that occurred during processing, if any
+ /// </summary>
+ public Exception? Error { get; init; }
+}
diff --git a/foreign/csharp/Iggy_SDK/Contracts/MessageFetchRequest.cs b/foreign/csharp/Iggy_SDK/Contracts/MessageFetchRequest.cs
index 20aea88..7b1b456 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/MessageFetchRequest.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/MessageFetchRequest.cs
@@ -26,6 +26,6 @@
public required Identifier TopicId { get; init; }
public uint? PartitionId { get; init; }
public required PollingStrategy PollingStrategy { get; init; }
- public required int Count { get; init; }
+ public required uint Count { get; init; }
public required bool AutoCommit { get; init; }
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/Contracts/MessageResponse.cs b/foreign/csharp/Iggy_SDK/Contracts/MessageResponse.cs
index 23ddccd..a77be3d 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/MessageResponse.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/MessageResponse.cs
@@ -22,7 +22,7 @@
public sealed class MessageResponse
{
- public MessageHeader Header { get; set; }
- public byte[] Payload { get; set; } = [];
+ public required MessageHeader Header { get; set; }
+ public required byte[] Payload { get; set; } = [];
public Dictionary<HeaderKey, HeaderValue>? UserHeaders { get; init; }
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/Contracts/MessageResponseGeneric.cs b/foreign/csharp/Iggy_SDK/Contracts/MessageResponseGeneric.cs
index 4ee3380..dc55db0 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/MessageResponseGeneric.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/MessageResponseGeneric.cs
@@ -23,7 +23,7 @@
public sealed class MessageResponse<T>
{
- public MessageHeader Header { get; set; }
+ public required MessageHeader Header { get; set; }
public Dictionary<HeaderKey, HeaderValue>? UserHeaders { get; init; }
public required T Message { get; init; }
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/Contracts/PolledMessages.cs b/foreign/csharp/Iggy_SDK/Contracts/PolledMessages.cs
index 494bf31..7186824 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/PolledMessages.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/PolledMessages.cs
@@ -21,7 +21,7 @@
{
public required int PartitionId { get; init; }
public required ulong CurrentOffset { get; init; }
- public required IReadOnlyList<MessageResponse> Messages { get; init; }
+ public required IReadOnlyList<MessageResponse> Messages { get; set; }
public static PolledMessages Empty =>
new()
@@ -30,4 +30,4 @@
CurrentOffset = 0,
PartitionId = 0
};
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs b/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
index 631fc52..2ea87b7 100644
--- a/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
+++ b/foreign/csharp/Iggy_SDK/Contracts/Tcp/TcpContracts.cs
@@ -365,7 +365,7 @@
internal static void GetMessages(Span<byte> bytes, Consumer consumer, Identifier streamId, Identifier topicId,
PollingStrategy pollingStrategy,
- int count, bool autoCommit, uint? partitionId)
+ uint count, bool autoCommit, uint? partitionId)
{
bytes[0] = GetConsumerTypeByte(consumer.Type);
bytes.WriteBytesFromIdentifier(consumer.Id, 1);
@@ -375,7 +375,7 @@
BinaryPrimitives.WriteUInt32LittleEndian(bytes[position..(position + 4)], partitionId ?? 0);
bytes[position + 4] = GetPollingStrategyByte(pollingStrategy.Kind);
BinaryPrimitives.WriteUInt64LittleEndian(bytes[(position + 5)..(position + 13)], pollingStrategy.Value);
- BinaryPrimitives.WriteInt32LittleEndian(bytes[(position + 13)..(position + 17)], count);
+ BinaryPrimitives.WriteUInt32LittleEndian(bytes[(position + 13)..(position + 17)], count);
bytes[position + 17] = autoCommit ? (byte)1 : (byte)0;
}
@@ -805,7 +805,7 @@
bytes.WriteBytesFromIdentifier(consumer.Id, 1);
var position = 1 + consumer.Id.Length + 2;
bytes.WriteBytesFromStreamAndTopicIdentifiers(streamId, topicId, position);
- position = 7 + 2 + streamId.Length + 2 + topicId.Length;
+ position += 2 + streamId.Length + 2 + topicId.Length;
BinaryPrimitives.WriteUInt32LittleEndian(bytes[position..(position + 4)], partitionId ?? 0);
return bytes.ToArray();
}
diff --git a/foreign/csharp/Iggy_SDK/Encryption/AesMessageEncryptor.cs b/foreign/csharp/Iggy_SDK/Encryption/AesMessageEncryptor.cs
new file mode 100644
index 0000000..8bd8ec2
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Encryption/AesMessageEncryptor.cs
@@ -0,0 +1,128 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Security.Cryptography;
+
+namespace Apache.Iggy.Encryption;
+
+/// <summary>
+/// AES-256-GCM based message encryptor for secure message encryption/decryption.
+/// Uses AES-GCM (Galois/Counter Mode) which provides both confidentiality and authenticity.
+/// </summary>
+public sealed class AesMessageEncryptor : IMessageEncryptor
+{
+ private readonly byte[] _key;
+ private const int NonceSize = 12; // 96 bits - recommended for GCM
+ private const int TagSize = 16; // 128 bits authentication tag
+
+ /// <summary>
+ /// Creates a new AES message encryptor with the specified key.
+ /// </summary>
+ /// <param name="key">The encryption key. Must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256 respectively.</param>
+ /// <exception cref="ArgumentException">Thrown when key length is invalid</exception>
+ public AesMessageEncryptor(byte[] key)
+ {
+ if (key.Length != 16 && key.Length != 24 && key.Length != 32)
+ {
+ throw new ArgumentException("Key must be 16, 24, or 32 bytes for AES-128, AES-192, or AES-256", nameof(key));
+ }
+
+ _key = key;
+ }
+
+ /// <summary>
+ /// Creates a new AES-256 message encryptor with the specified base64-encoded key.
+ /// </summary>
+ /// <param name="base64Key">The base64-encoded encryption key</param>
+ /// <returns>A new AesMessageEncryptor instance</returns>
+ public static AesMessageEncryptor FromBase64Key(string base64Key)
+ {
+ return new AesMessageEncryptor(Convert.FromBase64String(base64Key));
+ }
+
+ /// <summary>
+ /// Generates a new random AES-256 key.
+ /// </summary>
+ /// <returns>A 32-byte random key suitable for AES-256</returns>
+ public static byte[] GenerateKey()
+ {
+ var key = new byte[32];
+ using var rng = RandomNumberGenerator.Create();
+ rng.GetBytes(key);
+ return key;
+ }
+
+ /// <summary>
+ /// Encrypts the provided plain data using AES-GCM.
+ /// Format: [12-byte nonce][encrypted data][16-byte authentication tag]
+ /// </summary>
+ /// <param name="plainData">The plain data to encrypt</param>
+ /// <returns>The encrypted data with nonce and tag</returns>
+ public byte[] Encrypt(byte[] plainData)
+ {
+ using var aesGcm = new AesGcm(_key, TagSize);
+
+ var nonce = new byte[NonceSize];
+ RandomNumberGenerator.Fill(nonce);
+
+ var ciphertext = new byte[plainData.Length];
+ var tag = new byte[TagSize];
+
+ aesGcm.Encrypt(nonce, plainData, ciphertext, tag);
+
+ // Combine: nonce + ciphertext + tag
+ var result = new byte[NonceSize + ciphertext.Length + TagSize];
+ Buffer.BlockCopy(nonce, 0, result, 0, NonceSize);
+ Buffer.BlockCopy(ciphertext, 0, result, NonceSize, ciphertext.Length);
+ Buffer.BlockCopy(tag, 0, result, NonceSize + ciphertext.Length, TagSize);
+
+ return result;
+ }
+
+ /// <summary>
+ /// Decrypts the provided encrypted data using AES-GCM.
+ /// Expected format: [12-byte nonce][encrypted data][16-byte authentication tag]
+ /// </summary>
+ /// <param name="encryptedData">The encrypted data with nonce and tag</param>
+ /// <returns>The decrypted plain data</returns>
+ /// <exception cref="ArgumentException">Thrown when encrypted data format is invalid</exception>
+ /// <exception cref="CryptographicException">Thrown when decryption or authentication fails</exception>
+ public byte[] Decrypt(byte[] encryptedData)
+ {
+ if (encryptedData.Length < NonceSize + TagSize)
+ {
+ throw new ArgumentException("Encrypted data is too short to contain nonce and tag", nameof(encryptedData));
+ }
+
+ using var aesGcm = new AesGcm(_key, TagSize);
+
+ // Extract nonce, ciphertext, and tag
+ var nonce = new byte[NonceSize];
+ var tag = new byte[TagSize];
+ var ciphertextLength = encryptedData.Length - NonceSize - TagSize;
+ var ciphertext = new byte[ciphertextLength];
+
+ Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize);
+ Buffer.BlockCopy(encryptedData, NonceSize, ciphertext, 0, ciphertextLength);
+ Buffer.BlockCopy(encryptedData, NonceSize + ciphertextLength, tag, 0, TagSize);
+
+ var plaintext = new byte[ciphertextLength];
+ aesGcm.Decrypt(nonce, ciphertext, tag, plaintext);
+
+ return plaintext;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Encryption/IMessageEncryptor.cs b/foreign/csharp/Iggy_SDK/Encryption/IMessageEncryptor.cs
new file mode 100644
index 0000000..fbb99c6
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Encryption/IMessageEncryptor.cs
@@ -0,0 +1,40 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+namespace Apache.Iggy.Encryption;
+
+/// <summary>
+/// Interface for encrypting and decrypting message payloads in Iggy messaging system.
+/// Implementations of this interface can be used with IggyPublisher and IggyConsumer
+/// to provide end-to-end encryption of message data.
+/// </summary>
+public interface IMessageEncryptor
+{
+ /// <summary>
+ /// Encrypts the provided plain data.
+ /// </summary>
+ /// <param name="plainData">The plain data to encrypt</param>
+ /// <returns>The encrypted data</returns>
+ byte[] Encrypt(byte[] plainData);
+
+ /// <summary>
+ /// Decrypts the provided encrypted data.
+ /// </summary>
+ /// <param name="encryptedData">The encrypted data to decrypt</param>
+ /// <returns>The decrypted plain data</returns>
+ byte[] Decrypt(byte[] encryptedData);
+}
diff --git a/foreign/csharp/Iggy_SDK/Exceptions/ConsumerGroupNotFoundException.cs b/foreign/csharp/Iggy_SDK/Exceptions/ConsumerGroupNotFoundException.cs
new file mode 100644
index 0000000..b434639
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Exceptions/ConsumerGroupNotFoundException.cs
@@ -0,0 +1,18 @@
+// // Licensed to the Apache Software Foundation (ASF) under one
+// // or more contributor license agreements. See the NOTICE file
+// // distributed with this work for additional information
+// // regarding copyright ownership. The ASF licenses this file
+// // to you under the Apache License, Version 2.0 (the
+// // "License")
+
+namespace Apache.Iggy.Exceptions;
+
+public class ConsumerGroupNotFoundException : Exception
+{
+ public string ConsumerGroupName { get; }
+
+ public ConsumerGroupNotFoundException(string consumerGroupName) : base($"Consumer Group {consumerGroupName} not found")
+ {
+ ConsumerGroupName = consumerGroupName;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs b/foreign/csharp/Iggy_SDK/Exceptions/ConsumerNotInitializedException.cs
similarity index 61%
copy from foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
copy to foreign/csharp/Iggy_SDK/Exceptions/ConsumerNotInitializedException.cs
index f33f1d3..91c6ed6 100644
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
+++ b/foreign/csharp/Iggy_SDK/Exceptions/ConsumerNotInitializedException.cs
@@ -15,12 +15,21 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
+namespace Apache.Iggy.Exceptions;
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal interface IMessageInvoker
+public sealed class ConsumerNotInitializedException : InvalidOperationException
{
- internal Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default);
-}
\ No newline at end of file
+ public ConsumerNotInitializedException()
+ : base("Consumer must be initialized before receiving messages. Call InitAsync() first.")
+ {
+ }
+
+ public ConsumerNotInitializedException(string message) : base(message)
+ {
+ }
+
+ public ConsumerNotInitializedException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs b/foreign/csharp/Iggy_SDK/Exceptions/IggyInvalidStatusCodeException.cs
similarity index 73%
rename from foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
rename to foreign/csharp/Iggy_SDK/Exceptions/IggyInvalidStatusCodeException.cs
index f33f1d3..6d4ee7e 100644
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
+++ b/foreign/csharp/Iggy_SDK/Exceptions/IggyInvalidStatusCodeException.cs
@@ -15,12 +15,14 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
+namespace Apache.Iggy.Exceptions;
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal interface IMessageInvoker
+public sealed class IggyInvalidStatusCodeException : Exception
{
- internal Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default);
-}
\ No newline at end of file
+ public int StatusCode { get; init; }
+
+ internal IggyInvalidStatusCodeException(int statusCode, string message) : base(message)
+ {
+ StatusCode = statusCode;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Exceptions/InvalidBaseAdressException.cs b/foreign/csharp/Iggy_SDK/Exceptions/InvalidBaseAdressException.cs
index ae205e0..d8d0e91 100644
--- a/foreign/csharp/Iggy_SDK/Exceptions/InvalidBaseAdressException.cs
+++ b/foreign/csharp/Iggy_SDK/Exceptions/InvalidBaseAdressException.cs
@@ -19,7 +19,7 @@
internal sealed class InvalidBaseAdressException : Exception
{
- internal InvalidBaseAdressException() : base("Invalid Base Adress, use ':' only to describe the port")
+ internal InvalidBaseAdressException() : base("Invalid Base Address, use ':' only to describe the port")
{
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs b/foreign/csharp/Iggy_SDK/Exceptions/InvalidConsumerGroupNameException.cs
similarity index 72%
copy from foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
copy to foreign/csharp/Iggy_SDK/Exceptions/InvalidConsumerGroupNameException.cs
index f33f1d3..8969e94 100644
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
+++ b/foreign/csharp/Iggy_SDK/Exceptions/InvalidConsumerGroupNameException.cs
@@ -1,4 +1,4 @@
-// Licensed to the Apache Software Foundation (ASF) under one
+// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
@@ -15,12 +15,11 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
+namespace Apache.Iggy.Exceptions;
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal interface IMessageInvoker
+public class InvalidConsumerGroupNameException : Exception
{
- internal Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default);
-}
\ No newline at end of file
+ public InvalidConsumerGroupNameException(string message) : base(message)
+ {
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs b/foreign/csharp/Iggy_SDK/Exceptions/PublisherNotInitializedException.cs
similarity index 61%
copy from foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
copy to foreign/csharp/Iggy_SDK/Exceptions/PublisherNotInitializedException.cs
index f33f1d3..2059831 100644
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/IMessageInvoker.cs
+++ b/foreign/csharp/Iggy_SDK/Exceptions/PublisherNotInitializedException.cs
@@ -15,12 +15,21 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
+namespace Apache.Iggy.Exceptions;
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal interface IMessageInvoker
+public sealed class PublisherNotInitializedException : InvalidOperationException
{
- internal Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default);
-}
\ No newline at end of file
+ public PublisherNotInitializedException()
+ : base("Publisher must be initialized before sending messages. Call InitAsync() first.")
+ {
+ }
+
+ public PublisherNotInitializedException(string message) : base(message)
+ {
+ }
+
+ public PublisherNotInitializedException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Exceptions/StreamNotFoundException.cs b/foreign/csharp/Iggy_SDK/Exceptions/StreamNotFoundException.cs
new file mode 100644
index 0000000..795c637
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Exceptions/StreamNotFoundException.cs
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Enums;
+
+namespace Apache.Iggy.Exceptions;
+
+public sealed class StreamNotFoundException : Exception
+{
+ public Identifier StreamId { get; }
+
+ public StreamNotFoundException(Identifier streamId)
+ : base($"Stream {streamId} does not exist and auto-creation is disabled")
+ {
+ StreamId = streamId;
+ }
+
+ public StreamNotFoundException(Identifier streamId, string message) : base(message)
+ {
+ StreamId = streamId;
+ }
+
+ public StreamNotFoundException(Identifier streamId, string message, Exception innerException)
+ : base(message, innerException)
+ {
+ StreamId = streamId;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Exceptions/TopicNotFoundException.cs b/foreign/csharp/Iggy_SDK/Exceptions/TopicNotFoundException.cs
new file mode 100644
index 0000000..f509bea
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Exceptions/TopicNotFoundException.cs
@@ -0,0 +1,46 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Enums;
+
+namespace Apache.Iggy.Exceptions;
+
+public sealed class TopicNotFoundException : Exception
+{
+ public Identifier TopicId { get; }
+ public Identifier StreamId { get; }
+
+ public TopicNotFoundException(Identifier topicId, Identifier streamId)
+ : base($"Topic {topicId} does not exist in stream {streamId} and auto-creation is disabled")
+ {
+ TopicId = topicId;
+ StreamId = streamId;
+ }
+
+ public TopicNotFoundException(Identifier topicId, Identifier streamId, string message) : base(message)
+ {
+ TopicId = topicId;
+ StreamId = streamId;
+ }
+
+ public TopicNotFoundException(Identifier topicId, Identifier streamId, string message, Exception innerException)
+ : base(message, innerException)
+ {
+ TopicId = topicId;
+ StreamId = streamId;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Extensions/IggyClientExtenstion.cs b/foreign/csharp/Iggy_SDK/Extensions/IggyClientExtenstion.cs
new file mode 100644
index 0000000..d5ab8bc
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Extensions/IggyClientExtenstion.cs
@@ -0,0 +1,44 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Consumers;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Kinds;
+using Apache.Iggy.Publishers;
+
+namespace Apache.Iggy.Extensions;
+
+public static class IggyClientExtenstion
+{
+ public static IggyConsumerBuilder CreateConsumerBuilder(this IIggyClient client, Identifier streamId,
+ Identifier topicId, Consumer consumer)
+ {
+ return IggyConsumerBuilder.Create(client, streamId, topicId, consumer);
+ }
+
+ public static IggyConsumerBuilder CreateConsumerBuilder<T>(this IIggyClient client, Identifier streamId,
+ Identifier topicId, Consumer consumer, IDeserializer<T> deserializer) where T : IDeserializer<T>
+ {
+ return IggyConsumerBuilder.Create(client, streamId, topicId, consumer);
+ }
+
+ public static IggyPublisherBuilder CreatePublisherBuilder(this IIggyClient client, Identifier streamId,
+ Identifier topicId)
+ {
+ return IggyPublisherBuilder.Create(client, streamId, topicId);
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Factory/HttpMessageStreamBuilder.cs b/foreign/csharp/Iggy_SDK/Factory/HttpMessageStreamBuilder.cs
deleted file mode 100644
index a8f3143..0000000
--- a/foreign/csharp/Iggy_SDK/Factory/HttpMessageStreamBuilder.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using System.Threading.Channels;
-using Apache.Iggy.Configuration;
-using Apache.Iggy.Contracts;
-using Apache.Iggy.IggyClient.Implementations;
-using Apache.Iggy.MessagesDispatcher;
-using Microsoft.Extensions.Logging;
-using HttpMessageInvoker = Apache.Iggy.MessagesDispatcher.HttpMessageInvoker;
-
-namespace Apache.Iggy.Factory;
-
-internal class HttpMessageStreamBuilder
-{
- private readonly HttpClient _client;
- private readonly ILoggerFactory _loggerFactory;
- private readonly MessageBatchingSettings _messageBatchingSettings;
- private readonly MessagePollingSettings _messagePollingSettings;
- private Channel<MessageSendRequest>? _channel;
- private HttpMessageInvoker? _messageInvoker;
- private MessageSenderDispatcher? _messageSenderDispatcher;
-
- internal HttpMessageStreamBuilder(HttpClient client, IMessageStreamConfigurator options,
- ILoggerFactory loggerFactory)
- {
- var sendMessagesOptions = new MessageBatchingSettings();
- var messagePollingOptions = new MessagePollingSettings();
- options.MessageBatchingSettings.Invoke(sendMessagesOptions);
- options.MessagePollingSettings.Invoke(messagePollingOptions);
- _messageBatchingSettings = sendMessagesOptions;
- _messagePollingSettings = messagePollingOptions;
- _client = client;
- _loggerFactory = loggerFactory;
- }
-
- //TODO - this channel will probably need to be refactored, to accept a lambda instead of MessageSendRequest
- internal HttpMessageStreamBuilder WithSendMessagesDispatcher()
- {
- if (_messageBatchingSettings.Enabled)
- {
- _channel = Channel.CreateBounded<MessageSendRequest>(_messageBatchingSettings.MaxRequests);
- _messageInvoker = new HttpMessageInvoker(_client);
- _messageSenderDispatcher =
- new MessageSenderDispatcher(_messageBatchingSettings, _channel, _messageInvoker, _loggerFactory);
- }
- else
- {
- _messageInvoker = new HttpMessageInvoker(_client);
- }
-
- return this;
- }
-
- internal HttpMessageStream Build()
- {
- _messageSenderDispatcher?.Start();
- return _messageBatchingSettings.Enabled switch
- {
- true => new HttpMessageStream(_client, _channel, _messagePollingSettings, _loggerFactory),
- false => new HttpMessageStream(_client, _channel, _messagePollingSettings, _loggerFactory, _messageInvoker)
- };
- }
-}
\ No newline at end of file
diff --git a/foreign/csharp/Iggy_SDK/Factory/IggyClientFactory.cs b/foreign/csharp/Iggy_SDK/Factory/IggyClientFactory.cs
new file mode 100644
index 0000000..f7602a7
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Factory/IggyClientFactory.cs
@@ -0,0 +1,79 @@
+// // Licensed to the Apache Software Foundation (ASF) under one
+// // or more contributor license agreements. See the NOTICE file
+// // distributed with this work for additional information
+// // regarding copyright ownership. The ASF licenses this file
+// // to you under the Apache License, Version 2.0 (the
+// // "License")
+
+using System.ComponentModel;
+using System.Net.Security;
+using System.Net.Sockets;
+using Apache.Iggy.Configuration;
+using Apache.Iggy.ConnectionStream;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Exceptions;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.IggyClient.Implementations;
+
+namespace Apache.Iggy.Factory;
+
+public static class IggyClientFactory
+{
+ public static IIggyClient CreateClient(IggyClientConfigurator options)
+ {
+ return options.Protocol switch
+ {
+ Protocol.Http => CreateIggyHttpClient(options),
+ Protocol.Tcp => CreateIggyTcpClient(options),
+ _ => throw new InvalidEnumArgumentException()
+ };
+ }
+
+ private static IIggyClient CreateIggyTcpClient(IggyClientConfigurator options)
+ {
+ return new TcpMessageStream(CreateTcpStream(options), options.LoggerFactory);
+ }
+
+ private static IIggyClient CreateIggyHttpClient(IggyClientConfigurator options)
+ {
+ return new HttpMessageStream(CreateHttpClient(options));
+ }
+
+ private static IConnectionStream CreateTcpStream(IggyClientConfigurator options)
+ {
+ var urlPortSplitter = options.BaseAddress.Split(":");
+ if (urlPortSplitter.Length > 2)
+ {
+ throw new InvalidBaseAdressException();
+ }
+
+ var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ socket.Connect(urlPortSplitter[0], int.Parse(urlPortSplitter[1]));
+ socket.SendBufferSize = options.SendBufferSize;
+ socket.ReceiveBufferSize = options.ReceiveBufferSize;
+ return options.TlsSettings.Enabled switch
+ {
+ true => CreateSslStreamAndAuthenticate(socket, options.TlsSettings),
+ false => new TcpConnectionStream(new NetworkStream(socket))
+ };
+ }
+
+ private static TcpTlsConnectionStream CreateSslStreamAndAuthenticate(Socket socket, TlsSettings tlsSettings)
+ {
+ var stream = new NetworkStream(socket);
+ var sslStream = new SslStream(stream);
+ if (tlsSettings.Authenticate)
+ {
+ sslStream.AuthenticateAsClient(tlsSettings.Hostname);
+ }
+
+ return new TcpTlsConnectionStream(sslStream);
+ }
+
+ private static HttpClient CreateHttpClient(IggyClientConfigurator options)
+ {
+ var client = new HttpClient();
+ client.BaseAddress = new Uri(options.BaseAddress);
+ return client;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Factory/MessageStreamFactory.cs b/foreign/csharp/Iggy_SDK/Factory/MessageStreamFactory.cs
deleted file mode 100644
index e575c60..0000000
--- a/foreign/csharp/Iggy_SDK/Factory/MessageStreamFactory.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using System.ComponentModel;
-using System.Net.Security;
-using System.Net.Sockets;
-using Apache.Iggy.Configuration;
-using Apache.Iggy.ConnectionStream;
-using Apache.Iggy.Enums;
-using Apache.Iggy.Exceptions;
-using Apache.Iggy.IggyClient;
-using Apache.Iggy.IggyClient.Implementations;
-using Microsoft.Extensions.Logging;
-
-namespace Apache.Iggy.Factory;
-
-public static class MessageStreamFactory
-{
- //TODO - this whole setup will have to be refactored later,when adding support for ASP.NET Core DI
- public static IIggyClient CreateMessageStream(Action<IMessageStreamConfigurator> options,
- ILoggerFactory loggerFactory)
- {
- var config = new MessageStreamConfigurator();
- options.Invoke(config);
-
- return config.Protocol switch
- {
- Protocol.Http => CreateHttpMessageStream(config, loggerFactory),
- Protocol.Tcp => CreateTcpMessageStream(config, loggerFactory),
- _ => throw new InvalidEnumArgumentException()
- };
- }
-
- private static TcpMessageStream CreateTcpMessageStream(IMessageStreamConfigurator options,
- ILoggerFactory loggerFactory)
- {
- var socket = CreateTcpStream(options);
- return new TcpMessageStreamBuilder(socket, options, loggerFactory)
- .WithSendMessagesDispatcher() //this internally resolves whether the message dispatcher is created or not.
- .Build();
- }
-
- private static IConnectionStream CreateTcpStream(IMessageStreamConfigurator options)
- {
- var urlPortSplitter = options.BaseAdress.Split(":");
- if (urlPortSplitter.Length > 2)
- {
- throw new InvalidBaseAdressException();
- }
-
- var tlsOptions = new TlsSettings();
- options.TlsSettings.Invoke(tlsOptions);
- var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
- socket.Connect(urlPortSplitter[0], int.Parse(urlPortSplitter[1]));
- socket.SendBufferSize = options.SendBufferSize;
- socket.ReceiveBufferSize = options.ReceiveBufferSize;
- return tlsOptions.Enabled switch
- {
- true => CreateSslStreamAndAuthenticate(socket, tlsOptions),
- false => new TcpConnectionStream(new NetworkStream(socket))
- };
- }
-
- private static TcpTlsConnectionStream CreateSslStreamAndAuthenticate(Socket socket, TlsSettings tlsSettings)
- {
- var stream = new NetworkStream(socket);
- var sslStream = new SslStream(stream);
- if (tlsSettings.Authenticate)
- {
- sslStream.AuthenticateAsClient(tlsSettings.Hostname);
- }
-
- return new TcpTlsConnectionStream(sslStream);
- }
-
- private static HttpMessageStream CreateHttpMessageStream(IMessageStreamConfigurator options,
- ILoggerFactory loggerFactory)
- {
- var client = CreateHttpClient(options);
- return new HttpMessageStreamBuilder(client, options, loggerFactory)
- .WithSendMessagesDispatcher() //this internally resolves whether the message dispatcher is created or not
- .Build();
- }
-
- private static HttpClient CreateHttpClient(IMessageStreamConfigurator options)
- {
- var client = new HttpClient();
- client.BaseAddress = new Uri(options.BaseAdress);
- return client;
- }
-}
\ No newline at end of file
diff --git a/foreign/csharp/Iggy_SDK/Factory/TcpMessageStreamBuilder.cs b/foreign/csharp/Iggy_SDK/Factory/TcpMessageStreamBuilder.cs
deleted file mode 100644
index 21d41ab..0000000
--- a/foreign/csharp/Iggy_SDK/Factory/TcpMessageStreamBuilder.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using System.Threading.Channels;
-using Apache.Iggy.Configuration;
-using Apache.Iggy.ConnectionStream;
-using Apache.Iggy.Contracts;
-using Apache.Iggy.IggyClient.Implementations;
-using Apache.Iggy.MessagesDispatcher;
-using Microsoft.Extensions.Logging;
-
-namespace Apache.Iggy.Factory;
-
-internal class TcpMessageStreamBuilder
-{
- private readonly ILoggerFactory _loggerFactory;
- private readonly MessageBatchingSettings _messageBatchingOptions;
- private readonly MessagePollingSettings _messagePollingSettings;
- private readonly IConnectionStream _stream;
- private Channel<MessageSendRequest>? _channel;
- private TcpMessageInvoker? _messageInvoker;
- private MessageSenderDispatcher? _messageSenderDispatcher;
-
- internal TcpMessageStreamBuilder(IConnectionStream stream, IMessageStreamConfigurator options,
- ILoggerFactory loggerFactory)
- {
- var sendMessagesOptions = new MessageBatchingSettings();
- var messagePollingOptions = new MessagePollingSettings();
- options.MessagePollingSettings.Invoke(messagePollingOptions);
- options.MessageBatchingSettings.Invoke(sendMessagesOptions);
- _messageBatchingOptions = sendMessagesOptions;
- _messagePollingSettings = messagePollingOptions;
- _stream = stream;
- _loggerFactory = loggerFactory;
- }
-
- //TODO - this channel will probably need to be refactored, to accept a lambda instead of MessageSendRequest
- internal TcpMessageStreamBuilder WithSendMessagesDispatcher()
- {
- if (_messageBatchingOptions.Enabled)
- {
- _channel = Channel.CreateBounded<MessageSendRequest>(_messageBatchingOptions.MaxRequests);
- _messageInvoker = new TcpMessageInvoker(_stream);
- _messageSenderDispatcher =
- new MessageSenderDispatcher(_messageBatchingOptions, _channel, _messageInvoker, _loggerFactory);
- }
- else
- {
- _messageInvoker = new TcpMessageInvoker(_stream);
- }
-
- return this;
- }
-
- internal TcpMessageStream Build()
- {
- _messageSenderDispatcher?.Start();
- return _messageBatchingOptions.Enabled switch
- {
- true => new TcpMessageStream(_stream, _channel, _messagePollingSettings, _loggerFactory),
- false => new TcpMessageStream(_stream, _channel, _messagePollingSettings, _loggerFactory, _messageInvoker)
- };
- }
-}
\ No newline at end of file
diff --git a/foreign/csharp/Iggy_SDK/Identifier.cs b/foreign/csharp/Iggy_SDK/Identifier.cs
index 2c8691c..0532f35 100644
--- a/foreign/csharp/Iggy_SDK/Identifier.cs
+++ b/foreign/csharp/Iggy_SDK/Identifier.cs
@@ -78,6 +78,26 @@
};
}
+ public uint GetUInt32()
+ {
+ if (Kind != IdKind.Numeric)
+ {
+ throw new InvalidOperationException("Identifier is not numeric");
+ }
+
+ return BinaryPrimitives.ReadUInt32LittleEndian(Value);
+ }
+
+ public string GetString()
+ {
+ if (Kind != IdKind.String)
+ {
+ throw new InvalidOperationException("Identifier is not string");
+ }
+
+ return Encoding.UTF8.GetString(Value);
+ }
+
public bool Equals(Identifier other)
{
return Kind == other.Kind && Value.Equals(other.Value);
@@ -92,4 +112,4 @@
{
return HashCode.Combine((int)Kind, Value);
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/IIggyClient.cs b/foreign/csharp/Iggy_SDK/IggyClient/IIggyClient.cs
index 1f0a2c0..59b3838 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/IIggyClient.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/IIggyClient.cs
@@ -18,6 +18,6 @@
namespace Apache.Iggy.IggyClient;
public interface IIggyClient : IIggyPublisher, IIggyStream, IIggyTopic, IIggyConsumer, IIggyOffset, IIggyConsumerGroup,
- IIggySystem, IIggyPartition, IIggyUsers, IIggyPersonalAccessToken
+ IIggySystem, IIggyPartition, IIggyUsers, IIggyPersonalAccessToken, IDisposable
{
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumer.cs b/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumer.cs
index 796cbb2..3aa4e8c 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumer.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumer.cs
@@ -23,28 +23,12 @@
public interface IIggyConsumer
{
Task<PolledMessages> PollMessagesAsync(Identifier streamId, Identifier topicId, uint? partitionId,
- Consumer consumer, PollingStrategy pollingStrategy, int count, bool autoCommit,
- Func<byte[], byte[]>? decryptor = null, CancellationToken token = default)
+ Consumer consumer, PollingStrategy pollingStrategy, uint count, bool autoCommit,
+ CancellationToken token = default);
+
+ Task<PolledMessages> PollMessagesAsync(MessageFetchRequest request, CancellationToken token = default)
{
- return PollMessagesAsync(new MessageFetchRequest
- {
- AutoCommit = autoCommit,
- Consumer = consumer,
- Count = count,
- PartitionId = partitionId,
- PollingStrategy = pollingStrategy,
- StreamId = streamId,
- TopicId = topicId
- }, decryptor, token);
+ return PollMessagesAsync(request.StreamId, request.TopicId, request.PartitionId, request.Consumer,
+ request.PollingStrategy, request.Count, request.AutoCommit, token);
}
-
- Task<PolledMessages> PollMessagesAsync(MessageFetchRequest request, Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default);
-
- Task<PolledMessages<TMessage>> PollMessagesAsync<TMessage>(MessageFetchRequest request,
- Func<byte[], TMessage> deserializer, Func<byte[], byte[]>? decryptor = null, CancellationToken token = default);
-
- IAsyncEnumerable<MessageResponse<TMessage>> PollMessagesAsync<TMessage>(PollMessagesRequest request,
- Func<byte[], TMessage> deserializer, Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default);
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumerGroup.cs b/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumerGroup.cs
index d6ce00d..78e4e2a 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumerGroup.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/IIggyConsumerGroup.cs
@@ -28,7 +28,7 @@
CancellationToken token = default);
Task<ConsumerGroupResponse?> CreateConsumerGroupAsync(Identifier streamId, Identifier topicId, string name,
- uint? groupId, CancellationToken token = default);
+ uint? groupId = null, CancellationToken token = default);
Task DeleteConsumerGroupAsync(Identifier streamId, Identifier topicId, Identifier groupId,
CancellationToken token = default);
@@ -38,4 +38,4 @@
Task LeaveConsumerGroupAsync(Identifier streamId, Identifier topicId, Identifier groupId,
CancellationToken token = default);
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/IIggyPublisher.cs b/foreign/csharp/Iggy_SDK/IggyClient/IIggyPublisher.cs
index 275688b..4197cd7 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/IIggyPublisher.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/IIggyPublisher.cs
@@ -15,8 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-using Apache.Iggy.Contracts;
-using Apache.Iggy.Headers;
using Apache.Iggy.Kinds;
using Apache.Iggy.Messages;
@@ -24,25 +22,9 @@
public interface IIggyPublisher
{
- Task SendMessagesAsync(Identifier streamId, Identifier topicId, Partitioning partitioning, Message[] messages,
- Func<byte[], byte[]>? encryptor = null, CancellationToken token = default)
- {
- return SendMessagesAsync(new MessageSendRequest
- {
- Messages = messages,
- Partitioning = partitioning,
- StreamId = streamId,
- TopicId = topicId
- }, encryptor, token);
- }
-
- Task SendMessagesAsync(MessageSendRequest request, Func<byte[], byte[]>? encryptor = null,
- CancellationToken token = default);
-
- Task SendMessagesAsync<TMessage>(MessageSendRequest<TMessage> request, Func<TMessage, byte[]> serializer,
- Func<byte[], byte[]>? encryptor = null, Dictionary<HeaderKey, HeaderValue>? headers = null,
+ Task SendMessagesAsync(Identifier streamId, Identifier topicId, Partitioning partitioning, IList<Message> messages,
CancellationToken token = default);
Task FlushUnsavedBufferAsync(Identifier streamId, Identifier topicId, uint partitionId, bool fsync,
CancellationToken token = default);
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/HttpMessageStream.cs b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/HttpMessageStream.cs
index 454cb7d..8584f11 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/HttpMessageStream.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/HttpMessageStream.cs
@@ -15,28 +15,22 @@
// specific language governing permissions and limitations
// under the License.
-using System.Buffers.Binary;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
-using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
-using System.Threading.Channels;
-using Apache.Iggy.Configuration;
using Apache.Iggy.Contracts;
using Apache.Iggy.Contracts.Auth;
using Apache.Iggy.Contracts.Http;
using Apache.Iggy.Contracts.Http.Auth;
using Apache.Iggy.Enums;
using Apache.Iggy.Exceptions;
-using Apache.Iggy.Headers;
using Apache.Iggy.Kinds;
using Apache.Iggy.Messages;
-using Apache.Iggy.MessagesDispatcher;
using Apache.Iggy.StringHandlers;
-using Microsoft.Extensions.Logging;
+using Partitioning = Apache.Iggy.Kinds.Partitioning;
namespace Apache.Iggy.IggyClient.Implementations;
@@ -44,26 +38,16 @@
{
private const string Context = "csharp-sdk";
- private readonly Channel<MessageSendRequest>? _channel;
private readonly HttpClient _httpClient;
//TODO - create mechanism for refreshing jwt token
//TODO - replace the HttpClient with IHttpClientFactory, when implementing support for ASP.NET Core DI
//TODO - the error handling pattern is pretty ugly, look into moving it into an extension method
private readonly JsonSerializerOptions _jsonSerializerOptions;
- private readonly ILogger<HttpMessageStream> _logger;
- private readonly IMessageInvoker? _messageInvoker;
- private readonly MessagePollingSettings _messagePollingSettings;
- internal HttpMessageStream(HttpClient httpClient, Channel<MessageSendRequest>? channel,
- MessagePollingSettings messagePollingSettings, ILoggerFactory loggerFactory,
- IMessageInvoker? messageInvoker = null)
+ internal HttpMessageStream(HttpClient httpClient)
{
_httpClient = httpClient;
- _channel = channel;
- _messagePollingSettings = messagePollingSettings;
- _messageInvoker = messageInvoker;
- _logger = loggerFactory.CreateLogger<HttpMessageStream>();
_jsonSerializerOptions = new JsonSerializerOptions
{
@@ -244,69 +228,28 @@
return null;
}
- public async Task SendMessagesAsync(MessageSendRequest request,
- Func<byte[], byte[]>? encryptor = null,
+ public async Task SendMessagesAsync(Identifier streamId, Identifier topicId, Partitioning partitioning,
+ IList<Message> messages,
CancellationToken token = default)
{
- if (encryptor is not null)
+ var request = new MessageSendRequest
{
- for (var i = 0; i < request.Messages.Count; i++)
- {
- request.Messages[i] = request.Messages[i] with { Payload = encryptor(request.Messages[i].Payload) };
- }
- }
-
- if (_messageInvoker is not null)
- {
- await _messageInvoker.SendMessagesAsync(request, token);
- return;
- }
-
- await _channel!.Writer.WriteAsync(request, token);
- }
-
- public async Task SendMessagesAsync<TMessage>(MessageSendRequest<TMessage> request,
- Func<TMessage, byte[]> serializer,
- Func<byte[], byte[]>? encryptor = null, Dictionary<HeaderKey, HeaderValue>? headers = null,
- CancellationToken token = default)
- {
- IList<TMessage> messages = request.Messages;
- //TODO - maybe get rid of this closure ?
- var sendRequest = new MessageSendRequest
- {
- StreamId = request.StreamId,
- TopicId = request.TopicId,
- Partitioning = request.Partitioning,
- Messages = messages.Select(message =>
- {
- return new Message
- {
- // TODO: message id
- Header = new MessageHeader { Id = 0 },
- UserHeaders = headers,
- Payload = encryptor is not null ? encryptor(serializer(message)) : serializer(message)
- };
- }).ToArray()
+ StreamId = streamId,
+ TopicId = topicId,
+ Partitioning = partitioning,
+ Messages = messages
};
+ var json = JsonSerializer.Serialize(request, _jsonSerializerOptions);
+ var data = new StringContent(json, Encoding.UTF8, "application/json");
- if (_messageInvoker is not null)
+ var response = await _httpClient.PostAsync($"/streams/{request.StreamId}/topics/{request.TopicId}/messages",
+ data,
+ token);
+
+ if (!response.IsSuccessStatusCode)
{
- try
- {
- await _messageInvoker.SendMessagesAsync(sendRequest, token);
- }
- catch
- {
- var partId = BinaryPrimitives.ReadInt32LittleEndian(sendRequest.Partitioning.Value);
- _logger.LogError(
- "Error encountered while sending messages - Stream ID:{StreamId}, Topic ID:{TopicId}, Partition ID: {PartitionId}",
- sendRequest.StreamId, sendRequest.TopicId, partId);
- }
-
- return;
+ await HandleResponseAsync(response);
}
-
- await _channel!.Writer.WriteAsync(sendRequest, token);
}
public async Task FlushUnsavedBufferAsync(Identifier streamId, Identifier topicId, uint partitionId, bool fsync,
@@ -318,17 +261,17 @@
if (!response.IsSuccessStatusCode)
{
- await HandleResponseAsync(response);
+ await HandleResponseAsync(response, true);
}
}
- public async Task<PolledMessages> PollMessagesAsync(MessageFetchRequest request,
- Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default)
+ public async Task<PolledMessages> PollMessagesAsync(Identifier streamId, Identifier topicId, uint? partitionId,
+ Consumer consumer,
+ PollingStrategy pollingStrategy, uint count, bool autoCommit, CancellationToken token = default)
{
- var url = CreateUrl(
- $"/streams/{request.StreamId}/topics/{request.TopicId}/messages?consumer_id={request.Consumer.Id}" +
- $"&partition_id={request.PartitionId}&kind={request.PollingStrategy.Kind}&value={request.PollingStrategy.Value}&count={request.Count}&auto_commit={request.AutoCommit}");
+ var partitionIdParam = partitionId.HasValue ? $"&partition_id={partitionId.Value}" : string.Empty;
+ var url = CreateUrl($"/streams/{streamId}/topics/{topicId}/messages?consumer_id={consumer.Id}" +
+ $"{partitionIdParam}&kind={pollingStrategy.Kind}&value={pollingStrategy.Value}&count={count}&auto_commit={autoCommit}");
var response = await _httpClient.GetAsync(url, token);
if (response.IsSuccessStatusCode)
@@ -336,119 +279,13 @@
var pollMessages = await response.Content.ReadFromJsonAsync<PolledMessages>(_jsonSerializerOptions, token)
?? PolledMessages.Empty;
- if (decryptor is null)
- {
- return pollMessages;
- }
-
- foreach (var message in pollMessages.Messages)
- {
- message.Payload = decryptor(message.Payload);
- }
-
return pollMessages;
}
- await HandleResponseAsync(response);
+ await HandleResponseAsync(response, true);
return PolledMessages.Empty;
}
- public async Task<PolledMessages<TMessage>> PollMessagesAsync<TMessage>(MessageFetchRequest request,
- Func<byte[], TMessage> deserializer, Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default)
- {
- var url = CreateUrl(
- $"/streams/{request.StreamId}/topics/{request.TopicId}/messages?consumer_id={request.Consumer.Id}" +
- $"&partition_id={request.PartitionId}&kind={request.PollingStrategy.Kind}&value={request.PollingStrategy.Value}&count={request.Count}&auto_commit={request.AutoCommit}");
-
- var response = await _httpClient.GetAsync(url, token);
- if (response.IsSuccessStatusCode)
- {
- var pollMessages = await response.Content.ReadFromJsonAsync<PolledMessages>(_jsonSerializerOptions, token)
- ?? PolledMessages.Empty;
-
- var messages = new List<MessageResponse<TMessage>>();
- foreach (var message in pollMessages.Messages)
- {
- if (decryptor is not null)
- {
- message.Payload = decryptor(message.Payload);
- }
-
- messages.Add(new MessageResponse<TMessage>
- {
- Message = deserializer(message.Payload),
- Header = message.Header,
- UserHeaders = message.UserHeaders
- });
- }
-
- return new PolledMessages<TMessage>
- {
- PartitionId = pollMessages.PartitionId,
- CurrentOffset = pollMessages.CurrentOffset,
- Messages = messages
- };
- }
-
- await HandleResponseAsync(response);
- return PolledMessages<TMessage>.Empty;
- }
-
- public async IAsyncEnumerable<MessageResponse<TMessage>> PollMessagesAsync<TMessage>(PollMessagesRequest request,
- Func<byte[], TMessage> deserializer, Func<byte[], byte[]>? decryptor = null,
- [EnumeratorCancellation] CancellationToken token = default)
- {
- var channel = Channel.CreateUnbounded<MessageResponse<TMessage>>();
- var autoCommit = _messagePollingSettings.StoreOffsetStrategy switch
- {
- StoreOffset.Never => false,
- StoreOffset.WhenMessagesAreReceived => true,
- StoreOffset.AfterProcessingEachMessage => false,
- _ => throw new ArgumentOutOfRangeException()
- };
- var fetchRequest = new MessageFetchRequest
- {
- Consumer = request.Consumer,
- StreamId = request.StreamId,
- TopicId = request.TopicId,
- AutoCommit = autoCommit,
- Count = request.Count,
- PartitionId = request.PartitionId,
- PollingStrategy = request.PollingStrategy
- };
-
-
- _ = StartPollingMessagesAsync(fetchRequest, deserializer, _messagePollingSettings.Interval, channel.Writer,
- decryptor, token);
- await foreach (MessageResponse<TMessage> messageResponse in channel.Reader.ReadAllAsync(token))
- {
- yield return messageResponse;
-
- var currentOffset = messageResponse.Header.Offset;
- if (_messagePollingSettings.StoreOffsetStrategy is StoreOffset.AfterProcessingEachMessage)
- {
- try
- {
- await StoreOffsetAsync(request.Consumer, request.StreamId, request.TopicId, currentOffset,
- request.PartitionId, token);
- }
- catch
- {
- _logger.LogError(
- "Error encountered while saving offset information - Offset: {Offset}, Stream ID: {StreamId}, Topic ID: {TopicId}, Partition ID: {PartitionId}",
- currentOffset, request.StreamId, request.TopicId, request.PartitionId);
- }
- }
-
- if (request.PollingStrategy.Kind is MessagePolling.Offset)
- {
- //TODO - check with profiler whether this doesn't cause a lot of allocations
- request.PollingStrategy = PollingStrategy.Offset(currentOffset + 1);
- }
- }
- }
-
public async Task StoreOffsetAsync(Consumer consumer, Identifier streamId, Identifier topicId, ulong offset,
uint? partitionId, CancellationToken token = default)
{
@@ -467,8 +304,9 @@
public async Task<OffsetResponse?> GetOffsetAsync(Consumer consumer, Identifier streamId, Identifier topicId,
uint? partitionId, CancellationToken token = default)
{
+ var partitionIdParam = partitionId.HasValue ? $"&partition_id={partitionId.Value}" : string.Empty;
var response = await _httpClient.GetAsync($"/streams/{streamId}/topics/{topicId}/" +
- $"consumer-offsets?consumer_id={consumer.Id}&partition_id={partitionId}",
+ $"consumer-offsets?consumer_id={consumer.Id}{partitionIdParam}",
token);
if (response.IsSuccessStatusCode)
{
@@ -482,8 +320,9 @@
public async Task DeleteOffsetAsync(Consumer consumer, Identifier streamId, Identifier topicId, uint? partitionId,
CancellationToken token = default)
{
+ var partitionIdParam = partitionId.HasValue ? $"?partition_id={partitionId.Value}" : string.Empty;
var response = await _httpClient.DeleteAsync(
- $"/streams/{streamId}/topics/{topicId}/consumer-offsets/{consumer}?partition_id={partitionId}", token);
+ $"/streams/{streamId}/topics/{topicId}/consumer-offsets/{consumer}{partitionIdParam}", token);
await HandleResponseAsync(response);
}
@@ -728,7 +567,7 @@
public async Task<AuthResponse?> LoginUser(string userName, string password, CancellationToken token = default)
{
- // TODO: get version
+ // TODO: get version
var json = JsonSerializer.Serialize(new LoginUserRequest(userName, password, "", Context),
_jsonSerializerOptions);
@@ -834,42 +673,16 @@
return null;
}
- private async Task StartPollingMessagesAsync<TMessage>(MessageFetchRequest request,
- Func<byte[], TMessage> deserializer, TimeSpan interval, ChannelWriter<MessageResponse<TMessage>> writer,
- Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default)
+ public void Dispose()
{
- var timer = new PeriodicTimer(interval);
- while (await timer.WaitForNextTickAsync(token) || token.IsCancellationRequested)
- {
- try
- {
- PolledMessages<TMessage> fetchResponse
- = await PollMessagesAsync(request, deserializer, decryptor, token);
- if (fetchResponse.Messages.Count == 0)
- {
- continue;
- }
-
- foreach (MessageResponse<TMessage> messageResponse in fetchResponse.Messages)
- {
- await writer.WriteAsync(messageResponse, token);
- }
- }
- catch (Exception e)
- {
- _logger.LogError(e,
- "Error encountered while polling messages - Stream ID: {StreamId}, Topic ID: {TopicId}, Partition ID: {PartitionId}",
- request.StreamId, request.TopicId, request.PartitionId);
- }
- }
-
- writer.Complete();
}
- private static async Task HandleResponseAsync(HttpResponseMessage response)
+ private static async Task HandleResponseAsync(HttpResponseMessage response, bool shouldThrowOnGetNotFound = false)
{
- if ((int)response.StatusCode > 300 && (int)response.StatusCode < 500)
+ if ((int)response.StatusCode > 300
+ && (int)response.StatusCode < 500
+ && !(response.RequestMessage!.Method == HttpMethod.Get && response.StatusCode == HttpStatusCode.NotFound &&
+ !shouldThrowOnGetNotFound))
{
var err = await response.Content.ReadAsStringAsync();
throw new InvalidResponseException(err);
@@ -885,4 +698,4 @@
{
return message.ToString();
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
index 4294409..640703b 100644
--- a/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
+++ b/foreign/csharp/Iggy_SDK/IggyClient/Implementations/TcpMessageStream.cs
@@ -15,45 +15,33 @@
// specific language governing permissions and limitations
// under the License.
+using System.Buffers;
using System.Buffers.Binary;
-using System.IO.Hashing;
-using System.Runtime.CompilerServices;
using System.Text;
-using System.Threading.Channels;
-using Apache.Iggy.Configuration;
using Apache.Iggy.ConnectionStream;
using Apache.Iggy.Contracts;
using Apache.Iggy.Contracts.Auth;
using Apache.Iggy.Contracts.Tcp;
using Apache.Iggy.Enums;
using Apache.Iggy.Exceptions;
-using Apache.Iggy.Headers;
using Apache.Iggy.Kinds;
using Apache.Iggy.Mappers;
using Apache.Iggy.Messages;
-using Apache.Iggy.MessagesDispatcher;
using Apache.Iggy.Utils;
using Microsoft.Extensions.Logging;
+using Partitioning = Apache.Iggy.Kinds.Partitioning;
namespace Apache.Iggy.IggyClient.Implementations;
public sealed class TcpMessageStream : IIggyClient, IDisposable
{
- private readonly Channel<MessageSendRequest>? _channel;
private readonly ILogger<TcpMessageStream> _logger;
- private readonly IMessageInvoker? _messageInvoker;
- private readonly MessagePollingSettings _messagePollingSettings;
private readonly SemaphoreSlim _semaphore;
private readonly IConnectionStream _stream;
- internal TcpMessageStream(IConnectionStream stream, Channel<MessageSendRequest>? channel,
- MessagePollingSettings messagePollingSettings, ILoggerFactory loggerFactory,
- IMessageInvoker? messageInvoker = null)
+ internal TcpMessageStream(IConnectionStream stream, ILoggerFactory loggerFactory)
{
_stream = stream;
- _channel = channel;
- _messagePollingSettings = messagePollingSettings;
- _messageInvoker = messageInvoker;
_logger = loggerFactory.CreateLogger<TcpMessageStream>();
_semaphore = new SemaphoreSlim(1, 1);
}
@@ -65,7 +53,8 @@
_semaphore.Dispose();
}
- public async Task<StreamResponse?> CreateStreamAsync(string name, uint? streamId, CancellationToken token = default)
+ public async Task<StreamResponse?> CreateStreamAsync(string name, uint? streamId = null,
+ CancellationToken token = default)
{
var message = TcpContracts.CreateStream(name, streamId);
var payload = new byte[4 + BufferSizes.INITIAL_BYTES_LENGTH + message.Length];
@@ -91,7 +80,7 @@
if (responseBuffer.Length == 0)
{
- throw new InvalidResponseException("Received empty response while trying to get stream by ID.");
+ return null;
}
return BinaryMapper.MapStream(responseBuffer);
@@ -168,7 +157,7 @@
if (responseBuffer.Length == 0)
{
- throw new InvalidResponseException("Received empty response while trying to get topic by ID.");
+ return null;
}
return BinaryMapper.MapTopic(responseBuffer);
@@ -226,85 +215,33 @@
await SendWithResponseAsync(payload, token);
}
- public async Task SendMessagesAsync(MessageSendRequest request,
- Func<byte[], byte[]>? encryptor = null,
+ public async Task SendMessagesAsync(Identifier streamId, Identifier topicId, Partitioning partitioning,
+ IList<Message> messages,
CancellationToken token = default)
{
- if (request.Messages.Count == 0)
+ var metadataLength = 2 + streamId.Length + 2 + topicId.Length
+ + 2 + partitioning.Length + 4 + 4;
+ var messageBufferSize = TcpMessageStreamHelpers.CalculateMessageBytesCount(messages)
+ + metadataLength;
+ var payloadBufferSize = messageBufferSize + 4 + BufferSizes.INITIAL_BYTES_LENGTH;
+
+ IMemoryOwner<byte> messageBuffer = MemoryPool<byte>.Shared.Rent(messageBufferSize);
+ IMemoryOwner<byte> payloadBuffer = MemoryPool<byte>.Shared.Rent(payloadBufferSize);
+ try
{
- return;
+ TcpContracts.CreateMessage(messageBuffer.Memory.Span[..messageBufferSize], streamId,
+ topicId, partitioning, messages);
+
+ TcpMessageStreamHelpers.CreatePayload(payloadBuffer.Memory.Span[..payloadBufferSize],
+ messageBuffer.Memory.Span[..messageBufferSize], CommandCodes.SEND_MESSAGES_CODE);
+
+ await SendWithResponseAsync(payloadBuffer.Memory[..payloadBufferSize].ToArray(), token);
}
-
- //TODO - explore making fields of Message class mutable, so there is no need to create em from scratch
- if (encryptor is not null)
+ finally
{
- for (var i = 0; i < request.Messages.Count; i++)
- {
- request.Messages[i] = request.Messages[i] with { Payload = encryptor(request.Messages[i].Payload) };
- }
+ messageBuffer.Dispose();
+ payloadBuffer.Dispose();
}
-
- if (_messageInvoker is not null)
- {
- await _messageInvoker.SendMessagesAsync(request, token);
- return;
- }
-
- await _channel!.Writer.WriteAsync(request, token);
- }
-
- // TODO: Change TMessage implementation
- public async Task SendMessagesAsync<TMessage>(MessageSendRequest<TMessage> request,
- Func<TMessage, byte[]> serializer,
- Func<byte[], byte[]>? encryptor = null,
- Dictionary<HeaderKey, HeaderValue>? headers = null,
- CancellationToken token = default)
- {
- IList<TMessage> messages = request.Messages;
- if (messages.Count == 0)
- {
- return;
- }
-
- //TODO - explore making fields of Message class mutable, so there is no need to create em from scratch
- var messagesBuffer = new Message[messages.Count];
- for (var i = 0; i < messages.Count || token.IsCancellationRequested; i++)
- {
- var payload = encryptor is not null ? encryptor(serializer(messages[i])) : serializer(messages[i]);
- var checksum = BitConverter.ToUInt64(Crc64.Hash(payload));
-
- messagesBuffer[i] = new Message
- {
- Payload = payload,
- Header = new MessageHeader
- {
- Id = 0,
- Checksum = checksum,
- Offset = 0,
- OriginTimestamp = 0,
- Timestamp = DateTimeOffset.UtcNow,
- PayloadLength = payload.Length,
- UserHeadersLength = 0
- },
- UserHeaders = headers
- };
- }
-
- var sendRequest = new MessageSendRequest
- {
- StreamId = request.StreamId,
- TopicId = request.TopicId,
- Partitioning = request.Partitioning,
- Messages = messagesBuffer
- };
-
- if (_messageInvoker is not null)
- {
- await _messageInvoker.SendMessagesAsync(sendRequest, token);
- return;
- }
-
- await _channel!.Writer.WriteAsync(sendRequest, token);
}
public async Task FlushUnsavedBufferAsync(Identifier streamId, Identifier topicId, uint partitionId, bool fsync,
@@ -318,96 +255,23 @@
await SendWithResponseAsync(payload, token);
}
- public async Task<PolledMessages<TMessage>> PollMessagesAsync<TMessage>(MessageFetchRequest request,
- Func<byte[], TMessage> serializer, Func<byte[], byte[]>? decryptor = null, CancellationToken token = default)
+ public async Task<PolledMessages> PollMessagesAsync(Identifier streamId, Identifier topicId, uint? partitionId,
+ Consumer consumer,
+ PollingStrategy pollingStrategy, uint count, bool autoCommit, CancellationToken token = default)
{
- var messageBufferSize = CalculateMessageBufferSize(request.StreamId, request.TopicId, request.Consumer);
+ var messageBufferSize = CalculateMessageBufferSize(streamId, topicId, consumer);
var payloadBufferSize = CalculatePayloadBufferSize(messageBufferSize);
var message = new byte[messageBufferSize];
var payload = new byte[payloadBufferSize];
- TcpContracts.GetMessages(message.AsSpan()[..messageBufferSize], request.Consumer, request.StreamId,
- request.TopicId,
- request.PollingStrategy, request.Count, request.AutoCommit, request.PartitionId);
+ TcpContracts.GetMessages(message.AsSpan()[..messageBufferSize], consumer, streamId,
+ topicId, pollingStrategy, count, autoCommit, partitionId);
TcpMessageStreamHelpers.CreatePayload(payload, message.AsSpan()[..messageBufferSize],
CommandCodes.POLL_MESSAGES_CODE);
var responseBuffer = await SendWithResponseAsync(payload, token);
- return BinaryMapper.MapMessages(responseBuffer, serializer, decryptor);
- }
-
- public async IAsyncEnumerable<MessageResponse<TMessage>> PollMessagesAsync<TMessage>(PollMessagesRequest request,
- Func<byte[], TMessage> deserializer, Func<byte[], byte[]>? decryptor = null,
- [EnumeratorCancellation] CancellationToken token = default)
- {
- var channel = Channel.CreateUnbounded<MessageResponse<TMessage>>();
- var autoCommit = _messagePollingSettings.StoreOffsetStrategy switch
- {
- StoreOffset.Never => false,
- StoreOffset.WhenMessagesAreReceived => true,
- StoreOffset.AfterProcessingEachMessage => false,
- _ => throw new ArgumentOutOfRangeException()
- };
- var fetchRequest = new MessageFetchRequest
- {
- Consumer = request.Consumer,
- StreamId = request.StreamId,
- TopicId = request.TopicId,
- AutoCommit = autoCommit,
- Count = request.Count,
- PartitionId = request.PartitionId,
- PollingStrategy = request.PollingStrategy
- };
-
-
- _ = StartPollingMessagesAsync(fetchRequest, deserializer, _messagePollingSettings.Interval, channel.Writer,
- decryptor, token);
- await foreach (MessageResponse<TMessage> messageResponse in channel.Reader.ReadAllAsync(token))
- {
- yield return messageResponse;
-
- var currentOffset = messageResponse.Header.Offset;
- if (_messagePollingSettings.StoreOffsetStrategy is StoreOffset.AfterProcessingEachMessage)
- {
- try
- {
- await StoreOffsetAsync(request.Consumer, request.StreamId, request.TopicId, currentOffset,
- request.PartitionId, token);
- }
- catch
- {
- _logger.LogError(
- "Error encountered while saving offset information - Offset: {offset}, Stream ID: {streamId}, Topic ID: {topicId}, Partition ID: {partitionId}",
- currentOffset, request.StreamId, request.TopicId, request.PartitionId);
- }
- }
-
- if (request.PollingStrategy.Kind is MessagePolling.Offset)
- {
- //TODO - check with profiler whether this doesn't cause a lot of allocations
- request.PollingStrategy = PollingStrategy.Offset(currentOffset + 1);
- }
- }
- }
-
- public async Task<PolledMessages> PollMessagesAsync(MessageFetchRequest request,
- Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default)
- {
- var messageBufferSize = CalculateMessageBufferSize(request.StreamId, request.TopicId, request.Consumer);
- var payloadBufferSize = CalculatePayloadBufferSize(messageBufferSize);
- var message = new byte[messageBufferSize];
- var payload = new byte[payloadBufferSize];
-
- TcpContracts.GetMessages(message.AsSpan()[..messageBufferSize], request.Consumer, request.StreamId,
- request.TopicId, request.PollingStrategy, request.Count, request.AutoCommit, request.PartitionId);
- TcpMessageStreamHelpers.CreatePayload(payload, message.AsSpan()[..messageBufferSize],
- CommandCodes.POLL_MESSAGES_CODE);
-
- var responseBuffer = await SendWithResponseAsync(payload, token);
-
- return BinaryMapper.MapMessages(responseBuffer, decryptor);
+ return BinaryMapper.MapMessages(responseBuffer);
}
public async Task StoreOffsetAsync(Consumer consumer, Identifier streamId, Identifier topicId, ulong offset,
@@ -799,40 +663,40 @@
return new AuthResponse(userId, default);
}
- //TODO - look into calling the non generic FetchMessagesAsync method in order
- //to make this method re-usable for non generic PollMessages method.
- private async Task StartPollingMessagesAsync<TMessage>(MessageFetchRequest request,
- Func<byte[], TMessage> deserializer, TimeSpan interval, ChannelWriter<MessageResponse<TMessage>> writer,
- Func<byte[], byte[]>? decryptor = null,
- CancellationToken token = default)
- {
- var timer = new PeriodicTimer(interval);
- while (await timer.WaitForNextTickAsync(token) || token.IsCancellationRequested)
- {
- try
- {
- PolledMessages<TMessage> fetchResponse
- = await PollMessagesAsync(request, deserializer, decryptor, token);
- if (fetchResponse.Messages.Count == 0)
- {
- continue;
- }
-
- foreach (MessageResponse<TMessage> messageResponse in fetchResponse.Messages)
- {
- await writer.WriteAsync(messageResponse, token);
- }
- }
- catch (InvalidResponseException e)
- {
- _logger.LogError(
- "Error encountered while polling messages - Stream ID: {streamId}, Topic ID: {topicId}, Partition ID: {partitionId}, error message {message}",
- request.StreamId, request.TopicId, request.PartitionId, e.Message);
- }
- }
-
- writer.Complete();
- }
+ // //TODO - look into calling the non generic FetchMessagesAsync method in order
+ // //to make this method re-usable for non generic PollMessages method.
+ // private async Task StartPollingMessagesAsync<TMessage>(MessageFetchRequest request,
+ // Func<byte[], TMessage> deserializer, TimeSpan interval, ChannelWriter<MessageResponse<TMessage>> writer,
+ // Func<byte[], byte[]>? decryptor = null,
+ // CancellationToken token = default)
+ // {
+ // var timer = new PeriodicTimer(interval);
+ // while (await timer.WaitForNextTickAsync(token) || token.IsCancellationRequested)
+ // {
+ // try
+ // {
+ // PolledMessages<TMessage> fetchResponse
+ // = await PollMessagesAsync(request, deserializer, decryptor, token);
+ // if (fetchResponse.Messages.Count == 0)
+ // {
+ // continue;
+ // }
+ //
+ // foreach (MessageResponse<TMessage> messageResponse in fetchResponse.Messages)
+ // {
+ // await writer.WriteAsync(messageResponse, token);
+ // }
+ // }
+ // catch (InvalidResponseException e)
+ // {
+ // _logger.LogError(
+ // "Error encountered while polling messages - Stream ID: {streamId}, Topic ID: {topicId}, Partition ID: {partitionId}, error message {message}",
+ // request.StreamId, request.TopicId, request.PartitionId, e.Message);
+ // }
+ // }
+ //
+ // writer.Complete();
+ // }
private async Task<byte[]> SendWithResponseAsync(byte[] payload, CancellationToken token = default)
{
diff --git a/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj b/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
index 69e4a9c..86f5377 100644
--- a/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
+++ b/foreign/csharp/Iggy_SDK/Iggy_SDK.csproj
@@ -7,7 +7,7 @@
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>Apache.Iggy</AssemblyName>
<RootNamespace>Apache.Iggy</RootNamespace>
- <PackageVersion>0.6.0-edge.1</PackageVersion>
+ <PackageVersion>0.6.0-edge.2</PackageVersion>
</PropertyGroup>
<PropertyGroup>
@@ -35,6 +35,9 @@
<None Include="../LICENSE" Pack="true" PackagePath="">
<Link>Properties/LICENSE</Link>
</None>
+ <None Include="../DEPENDENCIES.md">
+ <Link>Properties\DEPENDENCIES.md</Link>
+ </None>
</ItemGroup>
<ItemGroup>
@@ -43,12 +46,6 @@
</ItemGroup>
<ItemGroup>
- <Content Include="..\DEPENDENCIES.md">
- <Link>Properties\DEPENDENCIES.md</Link>
- </Content>
- </ItemGroup>
-
- <ItemGroup>
<InternalsVisibleTo Include="Apache.Iggy.Tests" />
<InternalsVisibleTo Include="Apache.Iggy.Tests.Integrations" />
</ItemGroup>
diff --git a/foreign/csharp/Iggy_SDK/Kinds/PollingStrategy.cs b/foreign/csharp/Iggy_SDK/Kinds/PollingStrategy.cs
index 84a491b..6ca2784 100644
--- a/foreign/csharp/Iggy_SDK/Kinds/PollingStrategy.cs
+++ b/foreign/csharp/Iggy_SDK/Kinds/PollingStrategy.cs
@@ -19,11 +19,30 @@
namespace Apache.Iggy.Kinds;
+/// <summary>
+/// Represents a strategy for polling messages from a stream or topic.
+/// Defines the starting point for message consumption.
+/// </summary>
public readonly struct PollingStrategy
{
+ /// <summary>
+ /// Gets the type of message polling strategy to use.
+ /// </summary>
public required MessagePolling Kind { get; init; }
+
+ /// <summary>
+ /// Gets the value associated with the polling strategy.
+ /// For Offset: the message offset to start from.
+ /// For Timestamp: the Unix timestamp (in microseconds) to start from.
+ /// For First, Last, and Next: this value is 0.
+ /// </summary>
public required ulong Value { get; init; }
+ /// <summary>
+ /// Creates a polling strategy that starts from a specific message offset.
+ /// </summary>
+ /// <param name="value">The message offset to start polling from.</param>
+ /// <returns>A <see cref="PollingStrategy" /> configured for offset-based polling.</returns>
public static PollingStrategy Offset(ulong value)
{
return new PollingStrategy
@@ -33,6 +52,11 @@
};
}
+ /// <summary>
+ /// Creates a polling strategy that starts from a specific timestamp.
+ /// </summary>
+ /// <param name="value">The Unix timestamp (in microseconds) to start polling from.</param>
+ /// <returns>A <see cref="PollingStrategy" /> configured for timestamp-based polling.</returns>
public static PollingStrategy Timestamp(ulong value)
{
return new PollingStrategy
@@ -42,6 +66,10 @@
};
}
+ /// <summary>
+ /// Creates a polling strategy that starts from the first available message.
+ /// </summary>
+ /// <returns>A <see cref="PollingStrategy" /> configured to poll from the first message.</returns>
public static PollingStrategy First()
{
return new PollingStrategy
@@ -51,6 +79,10 @@
};
}
+ /// <summary>
+ /// Creates a polling strategy that starts from the last available message.
+ /// </summary>
+ /// <returns>A <see cref="PollingStrategy" /> configured to poll from the last message.</returns>
public static PollingStrategy Last()
{
return new PollingStrategy
@@ -60,6 +92,10 @@
};
}
+ /// <summary>
+ /// Creates a polling strategy that starts from the next available message after the current consumer offset.
+ /// </summary>
+ /// <returns>A <see cref="PollingStrategy" /> configured to poll from the next message.</returns>
public static PollingStrategy Next()
{
return new PollingStrategy
@@ -68,4 +104,4 @@
Value = 0
};
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/Messages/Message.cs b/foreign/csharp/Iggy_SDK/Messages/Message.cs
index d0cc4ef..499e4d9 100644
--- a/foreign/csharp/Iggy_SDK/Messages/Message.cs
+++ b/foreign/csharp/Iggy_SDK/Messages/Message.cs
@@ -25,11 +25,11 @@
namespace Apache.Iggy.Messages;
[JsonConverter(typeof(MessageConverter))]
-public readonly struct Message
+public class Message
{
public required MessageHeader Header { get; init; }
- public required byte[] Payload { get; init; }
- public Dictionary<HeaderKey, HeaderValue>? UserHeaders { get; init; }
+ public required byte[] Payload { get; set; }
+ public Dictionary<HeaderKey, HeaderValue>? UserHeaders { get; set; }
public Message()
{
@@ -71,4 +71,4 @@
{
return BitConverter.ToUInt64(Crc64.Hash(bytes));
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK/Messages/MessageHeader.cs b/foreign/csharp/Iggy_SDK/Messages/MessageHeader.cs
index 6dd37de..1112910 100644
--- a/foreign/csharp/Iggy_SDK/Messages/MessageHeader.cs
+++ b/foreign/csharp/Iggy_SDK/Messages/MessageHeader.cs
@@ -20,16 +20,16 @@
namespace Apache.Iggy.Messages;
-public readonly struct MessageHeader
+public class MessageHeader
{
- public ulong Checksum { get; init; }
- public UInt128 Id { get; init; }
- public ulong Offset { get; init; }
+ public ulong Checksum { get; set; }
+ public UInt128 Id { get; set; }
+ public ulong Offset { get; set; }
[JsonConverter(typeof(DateTimeOffsetConverter))]
- public DateTimeOffset Timestamp { get; init; }
+ public DateTimeOffset Timestamp { get; set; }
- public ulong OriginTimestamp { get; init; }
- public int UserHeadersLength { get; init; }
- public int PayloadLength { get; init; }
-}
\ No newline at end of file
+ public ulong OriginTimestamp { get; set; }
+ public int UserHeadersLength { get; set; }
+ public int PayloadLength { get; set; }
+}
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/HttpMessageInvoker.cs b/foreign/csharp/Iggy_SDK/MessagesDispatcher/HttpMessageInvoker.cs
deleted file mode 100644
index 16744de..0000000
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/HttpMessageInvoker.cs
+++ /dev/null
@@ -1,70 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using System.Net;
-using System.Text;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using Apache.Iggy.Contracts;
-using Apache.Iggy.Exceptions;
-
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal sealed class HttpMessageInvoker : IMessageInvoker
-{
- private readonly HttpClient _client;
- private readonly JsonSerializerOptions _jsonSerializerOptions;
-
- public HttpMessageInvoker(HttpClient client)
- {
- _client = client;
- _jsonSerializerOptions = new JsonSerializerOptions
- {
- PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
- Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) }
- };
- }
-
- public async Task SendMessagesAsync(MessageSendRequest request, CancellationToken token = default)
- {
- var json = JsonSerializer.Serialize(request, _jsonSerializerOptions);
- var data = new StringContent(json, Encoding.UTF8, "application/json");
-
- var response = await _client.PostAsync($"/streams/{request.StreamId}/topics/{request.TopicId}/messages", data,
- token);
- if (!response.IsSuccessStatusCode)
- {
- await HandleResponseAsync(response);
- }
- }
-
- private static async Task HandleResponseAsync(HttpResponseMessage response)
- {
- if ((int)response.StatusCode > 300 && (int)response.StatusCode < 500)
- {
- var err = await response.Content.ReadAsStringAsync();
- throw new InvalidResponseException(err);
- }
-
- if (response.StatusCode == HttpStatusCode.InternalServerError)
- {
- throw new Exception("HTTP Internal server error");
- }
-
- throw new Exception("Unknown error occurred.");
- }
-}
\ No newline at end of file
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/MessageSenderDispatcher.cs b/foreign/csharp/Iggy_SDK/MessagesDispatcher/MessageSenderDispatcher.cs
deleted file mode 100644
index 39e8458..0000000
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/MessageSenderDispatcher.cs
+++ /dev/null
@@ -1,214 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using System.Buffers;
-using System.Buffers.Binary;
-using System.Threading.Channels;
-using Apache.Iggy.Configuration;
-using Apache.Iggy.Contracts;
-using Apache.Iggy.Enums;
-using Apache.Iggy.Messages;
-using Microsoft.Extensions.Logging;
-
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal sealed class MessageSenderDispatcher
-{
- private readonly Channel<MessageSendRequest> _channel;
- private readonly CancellationTokenSource _cts = new();
- private readonly ILogger<MessageSenderDispatcher> _logger;
- private readonly int _maxMessagesPerBatch;
- private readonly int _maxRequests;
- private readonly IMessageInvoker _messageInvoker;
- private readonly PeriodicTimer _timer;
- private Task? _timerTask;
-
- internal MessageSenderDispatcher(MessageBatchingSettings sendMessagesOptions, Channel<MessageSendRequest> channel,
- IMessageInvoker messageInvoker, ILoggerFactory loggerFactory)
- {
- _timer = new PeriodicTimer(sendMessagesOptions.Interval);
- _logger = loggerFactory.CreateLogger<MessageSenderDispatcher>();
- _messageInvoker = messageInvoker;
- _maxMessagesPerBatch = sendMessagesOptions.MaxMessagesPerBatch;
- _maxRequests = sendMessagesOptions.MaxRequests;
- _channel = channel;
- }
-
- internal void Start()
- {
- _timerTask = SendMessages();
- }
-
- internal async Task SendMessages()
- {
- var messagesSendRequests = new MessageSendRequest[_maxRequests];
- while (await _timer.WaitForNextTickAsync(_cts.Token))
- {
- var idx = 0;
- while (_channel.Reader.TryRead(out var msg))
- {
- messagesSendRequests[idx++] = msg;
- }
-
- if (idx == 0)
- {
- continue;
- }
-
- var canBatchMessages = CanBatchMessages(messagesSendRequests.AsSpan()[..idx]);
- if (!canBatchMessages)
- {
- for (var i = 0; i < idx; i++)
- {
- try
- {
- await _messageInvoker.SendMessagesAsync(messagesSendRequests[i], _cts.Token);
- }
- catch
- {
- var partId = BinaryPrimitives.ReadInt32LittleEndian(messagesSendRequests[i].Partitioning.Value);
- _logger.LogError(
- "Error encountered while sending messages - Stream ID:{streamId}, Topic ID:{topicId}, Partition ID: {partitionId}",
- messagesSendRequests[i].StreamId, messagesSendRequests[i].TopicId, partId);
- }
- }
-
- continue;
- }
-
- MessageSendRequest[] messagesBatches = BatchMessages(messagesSendRequests.AsSpan()[..idx]);
- try
- {
- foreach (var messages in messagesBatches)
- {
- try
- {
- if (messages is null)
- {
- break;
- }
-
- await _messageInvoker.SendMessagesAsync(messages, _cts.Token);
- }
- catch
- {
- var partId = BinaryPrimitives.ReadInt32LittleEndian(messages.Partitioning.Value);
- _logger.LogError(
- "Error encountered while sending messages - Stream ID:{streamId}, Topic ID:{topicId}, Partition ID: {partitionId}",
- messages.StreamId, messages.TopicId, partId);
- }
- }
- }
- finally
- {
- ArrayPool<MessageSendRequest?>.Shared.Return(messagesBatches);
- }
- }
- }
-
- private static bool CanBatchMessages(ReadOnlySpan<MessageSendRequest> requests)
- {
- for (var i = 0; i < requests.Length - 1; i++)
- {
- var start = requests[i];
- var next = requests[i + 1];
-
- if (!start.StreamId.Equals(next.StreamId)
- || !start.TopicId.Equals(next.TopicId)
- || start.Partitioning.Kind is not Partitioning.PartitionId
- || !start.Partitioning.Value.SequenceEqual(next.Partitioning.Value))
- {
- return false;
- }
- }
-
- return true;
- }
-
- private MessageSendRequest[] BatchMessages(Span<MessageSendRequest> requests)
- {
- var messagesCount = 0;
- for (var i = 0; i < requests.Length; i++)
- {
- messagesCount += requests[i].Messages.Count;
- }
-
- var batchesCount = (int)Math.Ceiling((decimal)messagesCount / _maxMessagesPerBatch);
-
- Message[] messagesBuffer = ArrayPool<Message>.Shared.Rent(_maxMessagesPerBatch);
- Span<Message> messages = messagesBuffer.AsSpan()[.._maxMessagesPerBatch];
- MessageSendRequest[] messagesBatchesBuffer = ArrayPool<MessageSendRequest>.Shared.Rent(batchesCount);
-
- var idx = 0;
- var batchCounter = 0;
- try
- {
- foreach (var request in requests)
- {
- foreach (var message in request.Messages)
- {
- messages[idx++] = message;
- if (idx >= _maxMessagesPerBatch)
- {
- var messageSendRequest = new MessageSendRequest
- {
- Partitioning = request.Partitioning,
- StreamId = request.StreamId,
- TopicId = request.TopicId,
- Messages = messages.ToArray()
- };
- messagesBatchesBuffer[batchCounter] = messageSendRequest;
- batchCounter++;
- idx = 0;
- messages.Clear();
- }
- }
- }
-
- if (!messages.IsEmpty)
- {
- var messageSendRequest = new MessageSendRequest
- {
- Partitioning = requests[0].Partitioning,
- StreamId = requests[0].StreamId,
- TopicId = requests[0].TopicId,
- Messages = messages[..idx].ToArray()
- };
- messagesBatchesBuffer[batchCounter++] = messageSendRequest;
- }
-
- return messagesBatchesBuffer;
- }
- finally
- {
- ArrayPool<Message>.Shared.Return(messagesBuffer);
- }
- }
-
- internal async Task StopAsync()
- {
- if (_timerTask is null)
- {
- return;
- }
-
- _timer.Dispose();
- _cts.Cancel();
- await _timerTask;
- _cts.Dispose();
- }
-}
\ No newline at end of file
diff --git a/foreign/csharp/Iggy_SDK/MessagesDispatcher/TcpMessageInvoker.cs b/foreign/csharp/Iggy_SDK/MessagesDispatcher/TcpMessageInvoker.cs
deleted file mode 100644
index c315b00..0000000
--- a/foreign/csharp/Iggy_SDK/MessagesDispatcher/TcpMessageInvoker.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-using System.Buffers;
-using System.Text;
-using Apache.Iggy.ConnectionStream;
-using Apache.Iggy.Contracts;
-using Apache.Iggy.Contracts.Tcp;
-using Apache.Iggy.Exceptions;
-using Apache.Iggy.Messages;
-using Apache.Iggy.Utils;
-
-namespace Apache.Iggy.MessagesDispatcher;
-
-internal class TcpMessageInvoker : IMessageInvoker
-{
- private readonly IConnectionStream _stream;
-
- public TcpMessageInvoker(IConnectionStream stream)
- {
- _stream = stream;
- }
-
- public async Task SendMessagesAsync(MessageSendRequest request,
- CancellationToken token = default)
- {
- IList<Message> messages = request.Messages;
- // StreamId, TopicId, Partitioning, message count, metadata field
- var metadataLength = 2 + request.StreamId.Length + 2 + request.TopicId.Length
- + 2 + request.Partitioning.Length + 4 + 4;
- var messageBufferSize = TcpMessageStreamHelpers.CalculateMessageBytesCount(messages)
- + metadataLength;
- var payloadBufferSize = messageBufferSize + 4 + BufferSizes.INITIAL_BYTES_LENGTH;
-
- IMemoryOwner<byte> messageBuffer = MemoryPool<byte>.Shared.Rent(messageBufferSize);
- IMemoryOwner<byte> payloadBuffer = MemoryPool<byte>.Shared.Rent(payloadBufferSize);
- IMemoryOwner<byte> responseBuffer = MemoryPool<byte>.Shared.Rent(BufferSizes.EXPECTED_RESPONSE_SIZE);
- try
- {
- TcpContracts.CreateMessage(messageBuffer.Memory.Span[..messageBufferSize], request.StreamId,
- request.TopicId, request.Partitioning, messages);
-
- TcpMessageStreamHelpers.CreatePayload(payloadBuffer.Memory.Span[..payloadBufferSize],
- messageBuffer.Memory.Span[..messageBufferSize], CommandCodes.SEND_MESSAGES_CODE);
-
- await _stream.SendAsync(payloadBuffer.Memory[..payloadBufferSize], token);
- await _stream.FlushAsync(token);
- var readed = await _stream.ReadAsync(responseBuffer.Memory, token);
-
- if (readed == 0)
- {
- throw new InvalidResponseException("No response received from the server.");
- }
-
- var response = TcpMessageStreamHelpers.GetResponseLengthAndStatus(responseBuffer.Memory.Span);
- if (response.Status != 0)
- {
- if (response.Length == 0)
- {
- throw new InvalidResponseException($"Invalid response status code: {response.Status}");
- }
-
- var errorBuffer = new byte[response.Length];
- await _stream.ReadAsync(errorBuffer, token);
- throw new InvalidResponseException(Encoding.UTF8.GetString(errorBuffer));
- }
- }
- finally
- {
- messageBuffer.Dispose();
- payloadBuffer.Dispose();
- }
- }
-}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/BackgroundMessageProcessor.Logging.cs b/foreign/csharp/Iggy_SDK/Publishers/BackgroundMessageProcessor.Logging.cs
new file mode 100644
index 0000000..e24893c
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/BackgroundMessageProcessor.Logging.cs
@@ -0,0 +1,81 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Threading.Channels;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Messages;
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Internal background processor that handles asynchronous message batching and sending.
+/// Reads messages from a bounded channel and sends them in batches with retry support.
+/// </summary>
+internal sealed partial class BackgroundMessageProcessor : IAsyncDisposable
+{
+ // Logging methods
+ [LoggerMessage(EventId = 10,
+ Level = LogLevel.Debug,
+ Message = "Background message processor started")]
+ private partial void LogBackgroundProcessorStarted();
+
+ [LoggerMessage(EventId = 11,
+ Level = LogLevel.Debug,
+ Message = "Background message processor cancelled")]
+ private partial void LogBackgroundProcessorCancelled();
+
+ [LoggerMessage(EventId = 12,
+ Level = LogLevel.Debug,
+ Message = "Background message processor stopped")]
+ private partial void LogBackgroundProcessorStopped();
+
+ [LoggerMessage(EventId = 15,
+ Level = LogLevel.Debug,
+ Message = "Waiting for background task to complete")]
+ private partial void LogWaitingForBackgroundTask();
+
+ [LoggerMessage(EventId = 16,
+ Level = LogLevel.Debug,
+ Message = "Background task completed")]
+ private partial void LogBackgroundTaskCompleted();
+
+ [LoggerMessage(EventId = 300,
+ Level = LogLevel.Warning,
+ Message = "Failed to send batch of {Count} messages (attempt {Attempt}/{MaxAttempts}). Retrying in {Delay}ms")]
+ private partial void LogRetryingBatch(Exception exception, int count, int attempt, int maxAttempts, double delay);
+
+ [LoggerMessage(EventId = 301,
+ Level = LogLevel.Warning,
+ Message = "Background task did not complete within timeout")]
+ private partial void LogBackgroundTaskTimeout();
+
+ [LoggerMessage(EventId = 403,
+ Level = LogLevel.Error,
+ Message = "Failed to send batch of {Count} messages")]
+ private partial void LogFailedToSendBatch(Exception exception, int count);
+
+ [LoggerMessage(EventId = 404,
+ Level = LogLevel.Error,
+ Message = "Unexpected error in background message processor")]
+ private partial void LogBackgroundProcessorError(Exception exception);
+
+ [LoggerMessage(EventId = 405,
+ Level = LogLevel.Error,
+ Message = "Failed to send batch of {Count} messages after {Attempts} attempts")]
+ private partial void LogFailedToSendBatchAfterRetries(Exception exception, int count, int attempts);
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/BackgroundMessageProcessor.cs b/foreign/csharp/Iggy_SDK/Publishers/BackgroundMessageProcessor.cs
new file mode 100644
index 0000000..322cb3d
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/BackgroundMessageProcessor.cs
@@ -0,0 +1,262 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using System.Threading.Channels;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Messages;
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Internal background processor that handles asynchronous message batching and sending.
+/// Reads messages from a bounded channel and sends them in batches with retry support.
+/// </summary>
+internal sealed partial class BackgroundMessageProcessor : IAsyncDisposable
+{
+ private readonly CancellationTokenSource _cancellationTokenSource;
+ private readonly IIggyClient _client;
+ private readonly IggyPublisherConfig _config;
+ private readonly ILogger<BackgroundMessageProcessor> _logger;
+ private readonly Channel<Message> _messageChannel;
+ private Task? _backgroundTask;
+ private bool _disposed;
+
+ /// <summary>
+ /// Gets the channel writer for queuing messages to be sent.
+ /// </summary>
+ public ChannelWriter<Message> MessageWriter { get; }
+
+ /// <summary>
+ /// Gets the channel reader for consuming messages from the queue.
+ /// </summary>
+ public ChannelReader<Message> MessageReader { get; }
+
+ /// <summary>
+ /// Gets a value indicating whether the processor is currently sending messages.
+ /// </summary>
+ public bool IsSending { get; private set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="BackgroundMessageProcessor" /> class.
+ /// </summary>
+ /// <param name="client">The Iggy client used to send messages.</param>
+ /// <param name="config">Configuration settings for the publisher.</param>
+ /// <param name="logger">Logger instance for diagnostic output.</param>
+ public BackgroundMessageProcessor(IIggyClient client,
+ IggyPublisherConfig config,
+ ILogger<BackgroundMessageProcessor> logger)
+ {
+ _client = client;
+ _config = config;
+ _logger = logger;
+ _cancellationTokenSource = new CancellationTokenSource();
+
+ var options = new BoundedChannelOptions(_config.BackgroundQueueCapacity)
+ {
+ FullMode = BoundedChannelFullMode.Wait,
+ SingleReader = true,
+ SingleWriter = false
+ };
+
+ _messageChannel = Channel.CreateBounded<Message>(options);
+ MessageWriter = _messageChannel.Writer;
+ MessageReader = _messageChannel.Reader;
+ }
+
+ /// <summary>
+ /// Disposes the background processor, cancels ongoing operations,
+ /// and waits for the background task to complete.
+ /// </summary>
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ await _cancellationTokenSource.CancelAsync();
+
+ if (_backgroundTask != null)
+ {
+ LogWaitingForBackgroundTask();
+ try
+ {
+ await _backgroundTask.WaitAsync(_config.BackgroundDisposalTimeout);
+ LogBackgroundTaskCompleted();
+ }
+ catch (TimeoutException)
+ {
+ LogBackgroundTaskTimeout();
+ }
+ catch (Exception e)
+ {
+ LogBackgroundProcessorError(e);
+ }
+ }
+
+ MessageWriter.Complete();
+ _cancellationTokenSource.Dispose();
+
+ _disposed = true;
+ }
+
+ /// <summary>
+ /// Event raised when an unexpected error occurs in the background processor.
+ /// </summary>
+ public event EventHandler<PublisherErrorEventArgs>? OnBackgroundError;
+
+ /// <summary>
+ /// Event raised when a message batch fails to send after all retry attempts.
+ /// </summary>
+ public event EventHandler<MessageBatchFailedEventArgs>? OnMessageBatchFailed;
+
+ /// <summary>
+ /// Starts the background message processing task.
+ /// Does nothing if the processor is already running.
+ /// </summary>
+ public void Start()
+ {
+ if (_backgroundTask != null)
+ {
+ return;
+ }
+
+ _backgroundTask = RunBackgroundProcessor(_cancellationTokenSource.Token);
+ LogBackgroundProcessorStarted();
+ }
+
+ /// <summary>
+ /// Main background processing loop that reads messages from the channel,
+ /// batches them, and sends them periodically or when the batch size is reached.
+ /// </summary>
+ /// <param name="ct">Cancellation token to stop processing.</param>
+ private async Task RunBackgroundProcessor(CancellationToken ct)
+ {
+ var messageBatch = new List<Message>(_config.BackgroundBatchSize);
+ using var timer = new PeriodicTimer(_config.BackgroundFlushInterval);
+
+ try
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ while (messageBatch.Count < _config.BackgroundBatchSize &&
+ MessageReader.TryRead(out var message))
+ {
+ messageBatch.Add(message);
+ IsSending = true;
+ }
+
+ if (messageBatch.Count == 0)
+ {
+ if (!await timer.WaitForNextTickAsync(ct))
+ {
+ break;
+ }
+
+ continue;
+ }
+
+ await SendBatchWithRetry(messageBatch, ct);
+ messageBatch.Clear();
+ IsSending = false;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ LogBackgroundProcessorCancelled();
+ }
+ catch (Exception ex)
+ {
+ LogBackgroundProcessorError(ex);
+ OnBackgroundError?.Invoke(this,
+ new PublisherErrorEventArgs(ex, "Unexpected error in background message processor"));
+ }
+ finally
+ {
+ LogBackgroundProcessorStopped();
+ IsSending = false;
+ }
+ }
+
+ /// <summary>
+ /// Attempts to send a batch of messages with exponential backoff retry logic.
+ /// </summary>
+ /// <param name="messageBatch">The list of messages to send.</param>
+ /// <param name="ct">Cancellation token to cancel the operation.</param>
+ private async Task SendBatchWithRetry(List<Message> messageBatch, CancellationToken ct)
+ {
+ if (!_config.EnableRetry)
+ {
+ try
+ {
+ await _client.SendMessagesAsync(_config.StreamId, _config.TopicId, _config.Partitioning,
+ messageBatch.ToArray(), ct);
+ }
+ catch (Exception ex)
+ {
+ LogFailedToSendBatch(ex, messageBatch.Count);
+ OnMessageBatchFailed?.Invoke(this, new MessageBatchFailedEventArgs(ex, messageBatch.ToArray()));
+ }
+
+ return;
+ }
+
+ Exception? lastException = null;
+ var delay = _config.InitialRetryDelay;
+
+ for (var attempt = 0; attempt < _config.MaxRetryAttempts; attempt++)
+ {
+ try
+ {
+ await _client.SendMessagesAsync(_config.StreamId, _config.TopicId, _config.Partitioning,
+ messageBatch.ToArray(), ct);
+ return;
+ }
+ catch (Exception ex)
+ {
+ lastException = ex;
+
+ if (attempt < _config.MaxRetryAttempts && !ct.IsCancellationRequested)
+ {
+ LogRetryingBatch(ex, messageBatch.Count, attempt + 1, _config.MaxRetryAttempts + 1,
+ delay.TotalMilliseconds);
+ await Task.Delay(delay, ct);
+
+ var nextDelayMs = delay.TotalMilliseconds * _config.RetryBackoffMultiplier;
+
+ // Check for overflow or invalid values
+ if (double.IsInfinity(nextDelayMs) || double.IsNaN(nextDelayMs) ||
+ nextDelayMs > _config.MaxRetryDelay.TotalMilliseconds)
+ {
+ delay = _config.MaxRetryDelay;
+ }
+ else
+ {
+ // Ensure we don't exceed TimeSpan.MaxValue
+ delay = TimeSpan.FromMilliseconds(
+ Math.Min(nextDelayMs, TimeSpan.MaxValue.TotalMilliseconds));
+ }
+ }
+ }
+ }
+
+ LogFailedToSendBatchAfterRetries(lastException!, messageBatch.Count, _config.MaxRetryAttempts + 1);
+ OnMessageBatchFailed?.Invoke(this,
+ new MessageBatchFailedEventArgs(lastException!, messageBatch.ToArray(), _config.MaxRetryAttempts));
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/ISerializer.cs b/foreign/csharp/Iggy_SDK/Publishers/ISerializer.cs
new file mode 100644
index 0000000..0ef3740
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/ISerializer.cs
@@ -0,0 +1,54 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Interface for serializing objects of type T to byte arrays.
+/// <para>
+/// No type constraints are enforced on T to provide maximum flexibility.
+/// Implementations are responsible for ensuring that the provided type can be properly serialized.
+/// </para>
+/// </summary>
+/// <typeparam name="T">
+/// The source type for serialization. Can be any type - reference or value type, nullable or non-nullable.
+/// The serializer implementation must be able to handle the specific type provided.
+/// </typeparam>
+/// <remarks>
+/// Implementations should throw appropriate exceptions (e.g., <see cref="System.NotSupportedException" />,
+/// <see cref="System.ArgumentException" />, or <see cref="System.InvalidOperationException" />)
+/// if the provided data cannot be serialized. These exceptions will be caught and logged by
+/// <see cref="IggyPublisher{T}" /> during message creation.
+/// </remarks>
+public interface ISerializer<in T>
+{
+ /// <summary>
+ /// Serializes an instance of type T into a byte array.
+ /// </summary>
+ /// <param name="data">The object to serialize. May be null depending on the serializer implementation.</param>
+ /// <returns>A byte array representing the serialized data.</returns>
+ /// <exception cref="System.NotSupportedException">
+ /// Thrown when the serializer does not support the provided type or value.
+ /// </exception>
+ /// <exception cref="System.ArgumentException">
+ /// Thrown when the data cannot be serialized due to invalid content.
+ /// </exception>
+ /// <exception cref="System.InvalidOperationException">
+ /// Thrown when the serialization operation fails due to state issues.
+ /// </exception>
+ byte[] Serialize(T data);
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/IggyPublisher.Logging.cs b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisher.Logging.cs
new file mode 100644
index 0000000..3834847
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisher.Logging.cs
@@ -0,0 +1,139 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Publishers;
+
+public partial class IggyPublisher
+{
+ // Debug logs
+ [LoggerMessage(EventId = 1,
+ Level = LogLevel.Debug,
+ Message = "Initializing background message sending with queue capacity: {Capacity}, batch size: {BatchSize}")]
+ private partial void LogInitializingBackgroundSending(int capacity, int batchSize);
+
+ [LoggerMessage(EventId = 2,
+ Level = LogLevel.Debug,
+ Message = "Publisher already initialized")]
+ private partial void LogPublisherAlreadyInitialized();
+
+ [LoggerMessage(EventId = 3,
+ Level = LogLevel.Debug,
+ Message = "User {Login} logged in successfully")]
+ private partial void LogUserLoggedIn(string login);
+
+ [LoggerMessage(EventId = 4,
+ Level = LogLevel.Debug,
+ Message = "Stream {StreamId} already exists")]
+ private partial void LogStreamAlreadyExists(Identifier streamId);
+
+ [LoggerMessage(EventId = 5,
+ Level = LogLevel.Debug,
+ Message = "Topic {TopicId} already exists in stream {StreamId}")]
+ private partial void LogTopicAlreadyExists(Identifier topicId, Identifier streamId);
+
+ [LoggerMessage(EventId = 7,
+ Level = LogLevel.Debug,
+ Message = "Successfully sent {Count} messages")]
+ private partial void LogSuccessfullySentMessages(int count);
+
+ [LoggerMessage(EventId = 8,
+ Level = LogLevel.Debug,
+ Message = "Waiting for all pending messages to be sent")]
+ private partial void LogWaitingForPendingMessages();
+
+ [LoggerMessage(EventId = 9,
+ Level = LogLevel.Debug,
+ Message = "All pending messages have been sent")]
+ private partial void LogAllPendingMessagesSent();
+
+ [LoggerMessage(EventId = 14,
+ Level = LogLevel.Debug,
+ Message = "Disposing publisher")]
+ private partial void LogDisposingPublisher();
+
+ // Information logs
+ [LoggerMessage(EventId = 100,
+ Level = LogLevel.Information,
+ Message = "Initializing publisher for stream: {StreamId}, topic: {TopicId}")]
+ private partial void LogInitializingPublisher(Identifier streamId, Identifier topicId);
+
+ [LoggerMessage(EventId = 101,
+ Level = LogLevel.Information,
+ Message = "Background message sending started")]
+ private partial void LogBackgroundSendingStarted();
+
+ [LoggerMessage(EventId = 102,
+ Level = LogLevel.Information,
+ Message = "Publisher initialized successfully")]
+ private partial void LogPublisherInitialized();
+
+ [LoggerMessage(EventId = 103,
+ Level = LogLevel.Information,
+ Message = "Creating stream {StreamId} with name: {StreamName}")]
+ private partial void LogCreatingStream(Identifier streamId, string streamName);
+
+ [LoggerMessage(EventId = 104,
+ Level = LogLevel.Information,
+ Message = "Stream {StreamId} created successfully")]
+ private partial void LogStreamCreated(Identifier streamId);
+
+ [LoggerMessage(EventId = 105,
+ Level = LogLevel.Information,
+ Message = "Creating topic {TopicId} with name: {TopicName} in stream {StreamId}")]
+ private partial void LogCreatingTopic(Identifier topicId, string topicName, Identifier streamId);
+
+ [LoggerMessage(EventId = 106,
+ Level = LogLevel.Information,
+ Message = "Topic {TopicId} created successfully in stream {StreamId}")]
+ private partial void LogTopicCreated(Identifier topicId, Identifier streamId);
+
+
+ [LoggerMessage(EventId = 108,
+ Level = LogLevel.Information,
+ Message = "Publisher disposed")]
+ private partial void LogPublisherDisposed();
+
+ // Trace logs
+ [LoggerMessage(EventId = 200,
+ Level = LogLevel.Trace,
+ Message = "Queuing {Count} messages for background sending")]
+ private partial void LogQueuingMessages(int count);
+
+
+ // Error logs
+ [LoggerMessage(EventId = 400,
+ Level = LogLevel.Error,
+ Message = "Stream {StreamId} does not exist and auto-creation is disabled")]
+ private partial void LogStreamDoesNotExist(Identifier streamId);
+
+ [LoggerMessage(EventId = 401,
+ Level = LogLevel.Error,
+ Message = "Topic {TopicId} does not exist in stream {StreamId} and auto-creation is disabled")]
+ private partial void LogTopicDoesNotExist(Identifier topicId, Identifier streamId);
+
+ [LoggerMessage(EventId = 402,
+ Level = LogLevel.Error,
+ Message = "Attempted to send messages before publisher initialization")]
+ private partial void LogSendBeforeInitialization();
+
+ [LoggerMessage(EventId = 406,
+ Level = LogLevel.Error,
+ Message = "Failed to logout or dispose client")]
+ private partial void LogFailedToLogoutOrDispose(Exception exception);
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/IggyPublisher.cs b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisher.cs
new file mode 100644
index 0000000..8c95921
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisher.cs
@@ -0,0 +1,336 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Enums;
+using Apache.Iggy.Exceptions;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Messages;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// High-level publisher for sending messages to Iggy streams and topics.
+/// Supports background message batching, automatic retry, encryption, and stream/topic auto-creation.
+/// </summary>
+public partial class IggyPublisher : IAsyncDisposable
+{
+ private readonly BackgroundMessageProcessor? _backgroundProcessor;
+ private readonly IIggyClient _client;
+ private readonly IggyPublisherConfig _config;
+ private readonly ILogger<IggyPublisher> _logger;
+ private bool _disposed;
+ private bool _isInitialized;
+
+ /// <summary>
+ /// Gets the identifier of the stream this publisher sends messages to.
+ /// </summary>
+ public Identifier StreamId => _config.StreamId;
+
+ /// <summary>
+ /// Gets the identifier of the topic this publisher sends messages to.
+ /// </summary>
+ public Identifier TopicId => _config.TopicId;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="IggyPublisher" /> class.
+ /// </summary>
+ /// <param name="client">The Iggy client to use for communication.</param>
+ /// <param name="config">Publisher configuration settings.</param>
+ /// <param name="logger">Logger instance for diagnostic output.</param>
+ public IggyPublisher(IIggyClient client, IggyPublisherConfig config, ILogger<IggyPublisher> logger)
+ {
+ _client = client;
+ _config = config;
+ _logger = logger;
+
+ if (_config.EnableBackgroundSending)
+ {
+ LogInitializingBackgroundSending(_config.BackgroundQueueCapacity, _config.BackgroundBatchSize);
+
+ ILogger<BackgroundMessageProcessor> processorLogger
+ = _config.LoggerFactory?.CreateLogger<BackgroundMessageProcessor>()
+ ?? NullLogger<BackgroundMessageProcessor>.Instance;
+
+ _backgroundProcessor = new BackgroundMessageProcessor(_client, _config, processorLogger);
+ }
+ }
+
+ /// <summary>
+ /// Disposes the publisher, stops the background processor if running,
+ /// and logs out and disposes the client if it was created by the publisher.
+ /// </summary>
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ LogDisposingPublisher();
+
+ if (_backgroundProcessor != null)
+ {
+ await _backgroundProcessor.DisposeAsync();
+ }
+
+ if (_config.CreateIggyClient && _isInitialized)
+ {
+ try
+ {
+ await _client.LogoutUser();
+ _client.Dispose();
+ }
+ catch (Exception e)
+ {
+ LogFailedToLogoutOrDispose(e);
+ }
+ }
+
+ _disposed = true;
+ LogPublisherDisposed();
+ }
+
+ /// <summary>
+ /// Fired when any error occurs in the background task
+ /// </summary>
+ public event EventHandler<PublisherErrorEventArgs>? OnBackgroundError
+ {
+ add
+ {
+ if (_backgroundProcessor != null)
+ {
+ _backgroundProcessor.OnBackgroundError += value;
+ }
+ }
+ remove
+ {
+ if (_backgroundProcessor != null)
+ {
+ _backgroundProcessor.OnBackgroundError -= value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Fired when a batch of messages fails to send
+ /// </summary>
+ public event EventHandler<MessageBatchFailedEventArgs>? OnMessageBatchFailed
+ {
+ add
+ {
+ if (_backgroundProcessor != null)
+ {
+ _backgroundProcessor.OnMessageBatchFailed += value;
+ }
+ }
+ remove
+ {
+ if (_backgroundProcessor != null)
+ {
+ _backgroundProcessor.OnMessageBatchFailed -= value;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Initializes the publisher by authenticating, ensuring stream and topic exist,
+ /// and starting the background processor if enabled.
+ /// </summary>
+ /// <param name="ct">Cancellation token to cancel initialization.</param>
+ /// <exception cref="StreamNotFoundException">Thrown when the stream doesn't exist and auto-creation is disabled.</exception>
+ /// <exception cref="TopicNotFoundException">Thrown when the topic doesn't exist and auto-creation is disabled.</exception>
+ public async Task InitAsync(CancellationToken ct = default)
+ {
+ if (_isInitialized)
+ {
+ LogPublisherAlreadyInitialized();
+ return;
+ }
+
+
+ LogInitializingPublisher(_config.StreamId, _config.TopicId);
+ if (_config.CreateIggyClient)
+ {
+ await _client.LoginUser(_config.Login, _config.Password, ct);
+ LogUserLoggedIn(_config.Login);
+ }
+
+ await CreateStreamIfNeeded(ct);
+ await CreateTopicIfNeeded(ct);
+
+ if (_config.EnableBackgroundSending)
+ {
+ _backgroundProcessor?.Start();
+ LogBackgroundSendingStarted();
+ }
+
+ _isInitialized = true;
+ LogPublisherInitialized();
+ }
+
+ /// <summary>
+ /// Creates the stream if it doesn't exist and auto-creation is enabled in the configuration.
+ /// </summary>
+ /// <param name="ct">Cancellation token to cancel the operation.</param>
+ /// <exception cref="StreamNotFoundException">Thrown when the stream doesn't exist and auto-creation is disabled.</exception>
+ private async Task CreateStreamIfNeeded(CancellationToken ct)
+ {
+ if (await _client.GetStreamByIdAsync(_config.StreamId, ct) != null)
+ {
+ LogStreamAlreadyExists(_config.StreamId);
+ return;
+ }
+
+ if (!_config.CreateStream || string.IsNullOrEmpty(_config.StreamName))
+ {
+ LogStreamDoesNotExist(_config.StreamId);
+ throw new StreamNotFoundException(_config.StreamId);
+ }
+
+ LogCreatingStream(_config.StreamId, _config.StreamName);
+
+ if (_config.StreamId.Kind is IdKind.String)
+ {
+ await _client.CreateStreamAsync(_config.StreamId.GetString(), null, ct);
+ }
+ else
+ {
+ await _client.CreateStreamAsync(_config.StreamName, _config.StreamId.GetUInt32(), ct);
+ }
+
+ LogStreamCreated(_config.StreamId);
+ }
+
+ /// <summary>
+ /// Creates the topic if it doesn't exist and auto-creation is enabled in the configuration.
+ /// </summary>
+ /// <param name="ct">Cancellation token to cancel the operation.</param>
+ /// <exception cref="TopicNotFoundException">Thrown when the topic doesn't exist and auto-creation is disabled.</exception>
+ private async Task CreateTopicIfNeeded(CancellationToken ct)
+ {
+ if (await _client.GetTopicByIdAsync(_config.StreamId, _config.TopicId, ct) != null)
+ {
+ LogTopicAlreadyExists(_config.TopicId, _config.StreamId);
+ return;
+ }
+
+ if (!_config.CreateTopic || string.IsNullOrEmpty(_config.TopicName))
+ {
+ LogTopicDoesNotExist(_config.TopicId, _config.StreamId);
+ throw new TopicNotFoundException(_config.TopicId, _config.StreamId);
+ }
+
+ LogCreatingTopic(_config.TopicId, _config.TopicName, _config.StreamId);
+
+ if (_config.TopicId.Kind is IdKind.String)
+ {
+ await _client.CreateTopicAsync(_config.StreamId, _config.TopicId.GetString(),
+ _config.TopicPartitionsCount, _config.TopicCompressionAlgorithm, null,
+ _config.TopicReplicationFactor, _config.TopicMessageExpiry, _config.TopicMaxTopicSize, ct);
+ }
+ else
+ {
+ await _client.CreateTopicAsync(_config.StreamId, _config.TopicName, _config.TopicPartitionsCount,
+ _config.TopicCompressionAlgorithm, _config.TopicId.GetUInt32(), _config.TopicReplicationFactor,
+ _config.TopicMessageExpiry, _config.TopicMaxTopicSize, ct);
+ }
+
+ LogTopicCreated(_config.TopicId, _config.StreamId);
+ }
+
+ /// <summary>
+ /// Sends a collection of messages to the configured stream and topic.
+ /// If background sending is enabled, messages are queued for asynchronous processing.
+ /// Otherwise, messages are sent immediately.
+ /// </summary>
+ /// <param name="messages">The messages to send.</param>
+ /// <param name="ct">Cancellation token to cancel the send operation.</param>
+ /// <exception cref="PublisherNotInitializedException">Thrown when attempting to send before initialization.</exception>
+ public async Task SendMessages(IList<Message> messages, CancellationToken ct = default)
+ {
+ if (!_isInitialized)
+ {
+ LogSendBeforeInitialization();
+ throw new PublisherNotInitializedException();
+ }
+
+ if (messages.Count == 0)
+ {
+ return;
+ }
+
+ EncryptMessages(messages);
+
+ if (_config.EnableBackgroundSending && _backgroundProcessor != null)
+ {
+ LogQueuingMessages(messages.Count);
+ foreach (var message in messages)
+ {
+ await _backgroundProcessor.MessageWriter.WriteAsync(message, ct);
+ }
+ }
+ else
+ {
+ await _client.SendMessagesAsync(_config.StreamId, _config.TopicId, _config.Partitioning, messages, ct);
+ LogSuccessfullySentMessages(messages.Count);
+ }
+ }
+
+ /// <summary>
+ /// Waits until all queued messages have been sent by the background processor.
+ /// Only applicable when background sending is enabled. Returns immediately otherwise.
+ /// </summary>
+ /// <param name="ct">Cancellation token to cancel the wait operation.</param>
+ public async Task WaitUntilAllSends(CancellationToken ct = default)
+ {
+ if (!_config.EnableBackgroundSending || _backgroundProcessor == null)
+ {
+ return;
+ }
+
+ LogWaitingForPendingMessages();
+
+ while (_backgroundProcessor.MessageReader.Count > 0 ||
+ _backgroundProcessor.IsSending)
+ {
+ await Task.Delay(10, ct);
+ }
+
+ LogAllPendingMessagesSent();
+ }
+
+ /// <summary>
+ /// Encrypts all messages in the list using the configured message encryptor, if available.
+ /// Updates the payload length in the message header after encryption.
+ /// </summary>
+ /// <param name="messages">The messages to encrypt.</param>
+ private void EncryptMessages(IList<Message> messages)
+ {
+ if (_config.MessageEncryptor == null)
+ {
+ return;
+ }
+
+ foreach (var message in messages)
+ {
+ message.Payload = _config.MessageEncryptor.Encrypt(message.Payload);
+ message.Header.PayloadLength = message.Payload.Length;
+ }
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherBuilder.cs b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherBuilder.cs
new file mode 100644
index 0000000..09eea18
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherBuilder.cs
@@ -0,0 +1,425 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Encryption;
+using Apache.Iggy.Enums;
+using Apache.Iggy.Factory;
+using Apache.Iggy.IggyClient;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Partitioning = Apache.Iggy.Kinds.Partitioning;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Fluent builder for creating and configuring <see cref="IggyPublisher" /> instances.
+/// Provides a convenient API for setting up publishers with various configuration options
+/// including connection settings, partitioning, encryption, retry logic, and background sending.
+/// </summary>
+public class IggyPublisherBuilder
+{
+ protected EventHandler<PublisherErrorEventArgs>? _onBackgroundError;
+ protected EventHandler<MessageBatchFailedEventArgs>? _onMessageBatchFailed;
+
+ /// <summary>
+ /// Gets or sets the publisher configuration.
+ /// </summary>
+ internal IggyPublisherConfig Config { get; set; } = new();
+
+ /// <summary>
+ /// Gets or sets the Iggy client instance to use.
+ /// When null and <see cref="IggyPublisherConfig.CreateIggyClient" /> is true, a new client will be created during
+ /// build.
+ /// </summary>
+ internal IIggyClient? IggyClient { get; set; }
+
+ /// <summary>
+ /// Creates a new publisher builder using an existing Iggy client instance.
+ /// </summary>
+ /// <param name="iggyClient">The existing Iggy client to use.</param>
+ /// <param name="streamId">The identifier of the target stream.</param>
+ /// <param name="topicId">The identifier of the target topic.</param>
+ /// <returns>A new instance of <see cref="IggyPublisherBuilder" />.</returns>
+ public static IggyPublisherBuilder Create(IIggyClient iggyClient, Identifier streamId, Identifier topicId)
+ {
+ return new IggyPublisherBuilder
+ {
+ Config = new IggyPublisherConfig
+ {
+ CreateIggyClient = false,
+ StreamId = streamId,
+ TopicId = topicId
+ },
+ IggyClient = iggyClient
+ };
+ }
+
+ /// <summary>
+ /// Creates a new publisher builder that will create its own Iggy client.
+ /// </summary>
+ /// <param name="streamId">The identifier of the target stream.</param>
+ /// <param name="topicId">The identifier of the target topic.</param>
+ /// <returns>A new instance of <see cref="IggyPublisherBuilder" />.</returns>
+ public static IggyPublisherBuilder Create(Identifier streamId, Identifier topicId)
+ {
+ return new IggyPublisherBuilder
+ {
+ Config = new IggyPublisherConfig
+ {
+ CreateIggyClient = true,
+ StreamId = streamId,
+ TopicId = topicId
+ }
+ };
+ }
+
+ /// <summary>
+ /// Creates a new publisher builder using an existing configuration.
+ /// </summary>
+ /// <param name="config">The configuration to use for the publisher.</param>
+ /// <returns>A new instance of <see cref="IggyPublisherBuilder" />.</returns>
+ public static IggyPublisherBuilder Create(IggyPublisherConfig config)
+ {
+ return new IggyPublisherBuilder { Config = config };
+ }
+
+
+ /// <summary>
+ /// Configures the connection settings for the publisher's Iggy client.
+ /// Only used when the builder creates its own client.
+ /// </summary>
+ /// <param name="protocol">The protocol to use (TCP, QUIC, or HTTP).</param>
+ /// <param name="address">The server address to connect to (format depends on protocol).</param>
+ /// <param name="login">The login username for authentication.</param>
+ /// <param name="password">The password for authentication.</param>
+ /// <param name="receiveBufferSize">The size of the receive buffer in bytes. Default is 4096.</param>
+ /// <param name="sendBufferSize">The size of the send buffer in bytes. Default is 4096.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder WithConnection(Protocol protocol, string address, string login, string password,
+ int receiveBufferSize = 4096, int sendBufferSize = 4096)
+ {
+ Config.Protocol = protocol;
+ Config.Address = address;
+ Config.Login = login;
+ Config.Password = password;
+ Config.ReceiveBufferSize = receiveBufferSize;
+ Config.SendBufferSize = sendBufferSize;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Configures the partitioning strategy for messages sent by the publisher.
+ /// Determines how messages are distributed across topic partitions.
+ /// </summary>
+ /// <param name="partitioning">
+ /// The partitioning configuration to use (e.g., balanced, partition-specific, or message-key
+ /// based).
+ /// </param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder WithPartitioning(Partitioning partitioning)
+ {
+ Config.Partitioning = partitioning;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Enables automatic stream creation if the target stream does not exist.
+ /// </summary>
+ /// <param name="name">The name to use when creating the stream.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder CreateStreamIfNotExists(string name)
+ {
+ Config.CreateStream = true;
+ Config.StreamName = name;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Enables automatic topic creation if the target topic does not exist.
+ /// </summary>
+ /// <param name="name">The name to use when creating the topic.</param>
+ /// <param name="topicPartitionsCount">The number of partitions for the topic. Default is 1.</param>
+ /// <param name="compressionAlgorithm">The compression algorithm to use for messages in the topic. Default is None.</param>
+ /// <param name="replicationFactor">The replication factor for the topic. Null means server default.</param>
+ /// <param name="messageExpiry">The message expiry time in seconds (0 for no expiry). Default is 0.</param>
+ /// <param name="maxTopicSize">The maximum size of the topic in bytes (0 for unlimited). Default is 0.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder CreateTopicIfNotExists(string name, uint topicPartitionsCount = 1,
+ CompressionAlgorithm compressionAlgorithm = CompressionAlgorithm.None, byte? replicationFactor = null,
+ ulong messageExpiry = 0, ulong maxTopicSize = 0)
+ {
+ Config.CreateTopic = true;
+ Config.TopicName = name;
+ Config.TopicPartitionsCount = topicPartitionsCount;
+ Config.TopicCompressionAlgorithm = compressionAlgorithm;
+ Config.TopicReplicationFactor = replicationFactor;
+ Config.TopicMessageExpiry = messageExpiry;
+ Config.TopicMaxTopicSize = maxTopicSize;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Configures message encryption using the specified encryptor.
+ /// </summary>
+ /// <param name="encryptor">The message encryptor to use for encrypting message payloads.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder WithEncryptor(IMessageEncryptor encryptor)
+ {
+ Config.MessageEncryptor = encryptor;
+
+ return this;
+ }
+
+ /// <summary>
+ /// Registers an event handler for background processing errors.
+ /// Only invoked when background sending is enabled.
+ /// </summary>
+ /// <param name="handler">The event handler to invoke when background errors occur.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder OnBackgroundError(EventHandler<PublisherErrorEventArgs> handler)
+ {
+ _onBackgroundError = handler;
+ return this;
+ }
+
+ /// <summary>
+ /// Registers an event handler for failed message batch sends.
+ /// Invoked when a batch of messages fails to send after all retry attempts are exhausted.
+ /// </summary>
+ /// <param name="handler">The event handler to invoke when message batches fail to send.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder OnMessageBatchFailed(EventHandler<MessageBatchFailedEventArgs> handler)
+ {
+ _onMessageBatchFailed = handler;
+ return this;
+ }
+
+ /// <summary>
+ /// Configures retry behavior for failed message sends.
+ /// Uses exponential backoff with configurable parameters.
+ /// </summary>
+ /// <param name="enabled">Whether retry is enabled. Default is true.</param>
+ /// <param name="maxAttempts">The maximum number of retry attempts. Default is 3.</param>
+ /// <param name="initialDelay">The initial delay before the first retry. Default is 100ms.</param>
+ /// <param name="maxDelay">The maximum delay between retries. Default is 10 seconds.</param>
+ /// <param name="backoffMultiplier">The multiplier for exponential backoff. Default is 2.0.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder WithRetry(bool enabled = true, int maxAttempts = 3,
+ TimeSpan? initialDelay = null, TimeSpan? maxDelay = null, double backoffMultiplier = 2.0)
+ {
+ Config.EnableRetry = enabled;
+ Config.MaxRetryAttempts = maxAttempts;
+ Config.InitialRetryDelay = initialDelay ?? TimeSpan.FromMilliseconds(100);
+ Config.MaxRetryDelay = maxDelay ?? TimeSpan.FromSeconds(10);
+ Config.RetryBackoffMultiplier = backoffMultiplier;
+ return this;
+ }
+
+ /// <summary>
+ /// Configures background message sending for asynchronous, batched message delivery.
+ /// When enabled, messages are queued and sent in batches for improved throughput.
+ /// </summary>
+ /// <param name="enabled">Whether background sending is enabled. Default is true.</param>
+ /// <param name="queueCapacity">The maximum number of messages that can be queued. Default is 10,000.</param>
+ /// <param name="batchSize">The number of messages to send in each batch. Default is 100.</param>
+ /// <param name="flushInterval">The interval at which to flush pending messages. Default is 100ms.</param>
+ /// <param name="disposalTimeout">
+ /// The timeout to wait for the background processor to complete during disposal. Default is
+ /// 5 seconds.
+ /// </param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder WithBackgroundSending(bool enabled = true, int queueCapacity = 10000,
+ int batchSize = 100, TimeSpan? flushInterval = null, TimeSpan? disposalTimeout = null)
+ {
+ Config.EnableBackgroundSending = enabled;
+ Config.BackgroundQueueCapacity = queueCapacity;
+ Config.BackgroundBatchSize = batchSize;
+ Config.BackgroundFlushInterval = flushInterval ?? TimeSpan.FromMilliseconds(100);
+ Config.BackgroundDisposalTimeout = disposalTimeout ?? TimeSpan.FromSeconds(5);
+ return this;
+ }
+
+ /// <summary>
+ /// Configures the logger factory for diagnostic logging.
+ /// </summary>
+ /// <param name="loggerFactory">The logger factory to use for creating loggers.</param>
+ /// <returns>The builder instance for method chaining.</returns>
+ public IggyPublisherBuilder WithLogger(ILoggerFactory loggerFactory)
+ {
+ Config.LoggerFactory = loggerFactory;
+ return this;
+ }
+
+ /// <summary>
+ /// Builds and returns a configured <see cref="IggyPublisher" /> instance.
+ /// Creates the Iggy client if needed, wires up all event handlers, and initializes the publisher.
+ /// </summary>
+ /// <returns>A fully configured <see cref="IggyPublisher" /> instance ready to send messages.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when IggyClient is null and CreateIggyClient is false.</exception>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid.</exception>
+ public IggyPublisher Build()
+ {
+ Validate();
+
+ if (Config.CreateIggyClient)
+ {
+ IggyClient = IggyClientFactory.CreateClient(new IggyClientConfigurator
+ {
+ Protocol = Config.Protocol,
+ BaseAddress = Config.Address,
+ ReceiveBufferSize = Config.ReceiveBufferSize,
+ SendBufferSize = Config.SendBufferSize
+ });
+ }
+
+ var publisher = new IggyPublisher(IggyClient!, Config,
+ Config.LoggerFactory?.CreateLogger<IggyPublisher>() ??
+ NullLoggerFactory.Instance.CreateLogger<IggyPublisher>());
+
+ if (_onBackgroundError != null)
+ {
+ publisher.OnBackgroundError += _onBackgroundError;
+ }
+
+ if (_onMessageBatchFailed != null)
+ {
+ publisher.OnMessageBatchFailed += _onMessageBatchFailed;
+ }
+
+ return publisher;
+ }
+
+ /// <summary>
+ /// Validates the publisher configuration and throws if invalid.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid.</exception>
+ protected virtual void Validate()
+ {
+ if (Config.CreateIggyClient)
+ {
+ if (string.IsNullOrWhiteSpace(Config.Address))
+ {
+ throw new InvalidOperationException("Address must be provided when CreateIggyClient is true.");
+ }
+
+ if (string.IsNullOrWhiteSpace(Config.Login))
+ {
+ throw new InvalidOperationException("Login must be provided when CreateIggyClient is true.");
+ }
+
+ if (string.IsNullOrWhiteSpace(Config.Password))
+ {
+ throw new InvalidOperationException("Password must be provided when CreateIggyClient is true.");
+ }
+ }
+ else
+ {
+ if (IggyClient == null)
+ {
+ throw new InvalidOperationException(
+ "IggyClient must be provided when CreateIggyClient is false.");
+ }
+ }
+
+ if (Config.CreateStream && string.IsNullOrWhiteSpace(Config.StreamName))
+ {
+ throw new InvalidOperationException("StreamName must be provided when CreateStream is true.");
+ }
+
+ if (Config.CreateTopic)
+ {
+ if (string.IsNullOrWhiteSpace(Config.TopicName))
+ {
+ throw new InvalidOperationException("TopicName must be provided when CreateTopic is true.");
+ }
+
+ if (Config.TopicPartitionsCount == 0)
+ {
+ throw new InvalidOperationException("TopicPartitionsCount must be greater than 0.");
+ }
+ }
+
+ if (Config.ReceiveBufferSize <= 0)
+ {
+ throw new InvalidOperationException("ReceiveBufferSize must be greater than 0.");
+ }
+
+ if (Config.SendBufferSize <= 0)
+ {
+ throw new InvalidOperationException("SendBufferSize must be greater than 0.");
+ }
+
+ if (Config.EnableBackgroundSending)
+ {
+ if (Config.BackgroundQueueCapacity <= 0)
+ {
+ throw new InvalidOperationException(
+ "BackgroundQueueCapacity must be greater than 0 when EnableBackgroundSending is true.");
+ }
+
+ if (Config.BackgroundBatchSize <= 0)
+ {
+ throw new InvalidOperationException(
+ "BackgroundBatchSize must be greater than 0 when EnableBackgroundSending is true.");
+ }
+
+ if (Config.BackgroundFlushInterval <= TimeSpan.Zero)
+ {
+ throw new InvalidOperationException(
+ "BackgroundFlushInterval must be greater than zero when EnableBackgroundSending is true.");
+ }
+
+ if (Config.BackgroundDisposalTimeout <= TimeSpan.Zero)
+ {
+ throw new InvalidOperationException(
+ "BackgroundDisposalTimeout must be greater than zero when EnableBackgroundSending is true.");
+ }
+ }
+
+ if (Config.EnableRetry)
+ {
+ if (Config.MaxRetryAttempts <= 0)
+ {
+ throw new InvalidOperationException(
+ "MaxRetryAttempts must be greater than 0 when EnableRetry is true.");
+ }
+
+ if (Config.InitialRetryDelay <= TimeSpan.Zero)
+ {
+ throw new InvalidOperationException(
+ "InitialRetryDelay must be greater than zero when EnableRetry is true.");
+ }
+
+ if (Config.MaxRetryDelay <= TimeSpan.Zero)
+ {
+ throw new InvalidOperationException(
+ "MaxRetryDelay must be greater than zero when EnableRetry is true.");
+ }
+
+ if (Config.InitialRetryDelay > Config.MaxRetryDelay)
+ {
+ throw new InvalidOperationException(
+ "InitialRetryDelay must be less than or equal to MaxRetryDelay.");
+ }
+ }
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherBuilderOfT.cs b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherBuilderOfT.cs
new file mode 100644
index 0000000..c844716
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherBuilderOfT.cs
@@ -0,0 +1,141 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Configuration;
+using Apache.Iggy.Factory;
+using Apache.Iggy.IggyClient;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Builder for creating typed <see cref="IggyPublisher{T}" /> instances with fluent configuration
+/// </summary>
+/// <typeparam name="T">The type to serialize to message payloads</typeparam>
+public class IggyPublisherBuilder<T> : IggyPublisherBuilder
+{
+ /// <summary>
+ /// Creates a new typed publisher builder that will create its own Iggy client
+ /// </summary>
+ /// <param name="streamId">The stream identifier to publish to</param>
+ /// <param name="topicId">The topic identifier to publish to</param>
+ /// <param name="serializer">The serializer for converting objects to byte arrays</param>
+ /// <returns>A new instance of <see cref="IggyPublisherBuilder{T}" /></returns>
+ public static IggyPublisherBuilder<T> Create(Identifier streamId, Identifier topicId, ISerializer<T> serializer)
+ {
+ return new IggyPublisherBuilder<T>
+ {
+ Config = new IggyPublisherConfig<T>
+ {
+ CreateIggyClient = true,
+ StreamId = streamId,
+ TopicId = topicId,
+ Serializer = serializer
+ }
+ };
+ }
+
+ /// <summary>
+ /// Creates a new typed publisher builder using an existing Iggy client
+ /// </summary>
+ /// <param name="iggyClient">The existing Iggy client to use</param>
+ /// <param name="streamId">The stream identifier to publish to</param>
+ /// <param name="topicId">The topic identifier to publish to</param>
+ /// <param name="serializer">The serializer for converting objects to byte arrays</param>
+ /// <returns>A new instance of <see cref="IggyPublisherBuilder{T}" /></returns>
+ public static IggyPublisherBuilder<T> Create(IIggyClient iggyClient, Identifier streamId, Identifier topicId,
+ ISerializer<T> serializer)
+ {
+ return new IggyPublisherBuilder<T>
+ {
+ Config = new IggyPublisherConfig<T>
+ {
+ CreateIggyClient = false,
+ StreamId = streamId,
+ TopicId = topicId,
+ Serializer = serializer
+ },
+ IggyClient = iggyClient
+ };
+ }
+
+ /// <summary>
+ /// Builds and returns a typed <see cref="IggyPublisher{T}" /> instance with the configured settings
+ /// </summary>
+ /// <returns>A configured instance of <see cref="IggyPublisher{T}" /></returns>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid</exception>
+ public new IggyPublisher<T> Build()
+ {
+ Validate();
+
+ if (Config.CreateIggyClient)
+ {
+ IggyClient = IggyClientFactory.CreateClient(new IggyClientConfigurator
+ {
+ Protocol = Config.Protocol,
+ BaseAddress = Config.Address,
+ ReceiveBufferSize = Config.ReceiveBufferSize,
+ SendBufferSize = Config.SendBufferSize
+ });
+ }
+
+ if (Config is not IggyPublisherConfig<T> config)
+ {
+ throw new InvalidOperationException("Invalid publisher config");
+ }
+
+ var publisher = new IggyPublisher<T>(IggyClient!, config,
+ Config.LoggerFactory?.CreateLogger<IggyPublisher<T>>() ??
+ NullLoggerFactory.Instance.CreateLogger<IggyPublisher<T>>());
+
+ if (_onBackgroundError != null)
+ {
+ publisher.OnBackgroundError += _onBackgroundError;
+ }
+
+ if (_onMessageBatchFailed != null)
+ {
+ publisher.OnMessageBatchFailed += _onMessageBatchFailed;
+ }
+
+ return publisher;
+ }
+
+ /// <summary>
+ /// Validates the typed publisher configuration, including serializer validation.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown when the configuration is invalid.</exception>
+ protected override void Validate()
+ {
+ base.Validate();
+
+ if (Config is IggyPublisherConfig<T> typedConfig)
+ {
+ if (typedConfig.Serializer == null)
+ {
+ throw new InvalidOperationException(
+ $"Serializer must be provided for typed publisher IggyPublisher<{typeof(T).Name}>.");
+ }
+ }
+ else
+ {
+ throw new InvalidOperationException(
+ $"Config must be of type IggyPublisherConfig<{typeof(T).Name}>.");
+ }
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherConfig.cs b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherConfig.cs
new file mode 100644
index 0000000..a064b6b
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherConfig.cs
@@ -0,0 +1,257 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Encryption;
+using Apache.Iggy.Enums;
+using Microsoft.Extensions.Logging;
+using Partitioning = Apache.Iggy.Kinds.Partitioning;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Configuration for a typed Iggy publisher that serializes objects of type T to messages
+/// </summary>
+/// <typeparam name="T">The type to serialize to messages</typeparam>
+public class IggyPublisherConfig<T> : IggyPublisherConfig
+{
+ /// <summary>
+ /// Gets or sets the serializer used to convert objects of type T to message payloads.
+ /// This property is required and cannot be null. The builder will validate that a serializer
+ /// is provided before creating the publisher instance.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">
+ /// Thrown during publisher build if this property is null.
+ /// </exception>
+ public required ISerializer<T> Serializer { get; set; }
+}
+
+/// <summary>
+/// Configuration settings for <see cref="IggyPublisher" />.
+/// Provides comprehensive options for configuring message publishing behavior including
+/// connection settings, stream/topic management, background processing, and retry policies.
+/// </summary>
+public class IggyPublisherConfig
+{
+ /// <summary>
+ /// Gets or sets a value indicating whether the publisher should create its own Iggy client.
+ /// When true, the publisher will instantiate and manage its own client instance.
+ /// When false, an external client must be provided.
+ /// </summary>
+ public bool CreateIggyClient { get; set; }
+
+ /// <summary>
+ /// Gets or sets the protocol to use for communication (TCP, QUIC, or HTTP).
+ /// Only used when <see cref="CreateIggyClient" /> is true.
+ /// </summary>
+ public Protocol Protocol { get; set; }
+
+ /// <summary>
+ /// Gets or sets the server address to connect to.
+ /// Format depends on protocol (e.g., "localhost:8090" for TCP/QUIC, "http://localhost:3000" for HTTP).
+ /// Only used when <see cref="CreateIggyClient" /> is true.
+ /// </summary>
+ public string Address { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the login username for authentication.
+ /// Required if the Iggy server has authentication enabled.
+ /// </summary>
+ public string Login { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the password for authentication.
+ /// Required if the Iggy server has authentication enabled.
+ /// </summary>
+ public string Password { get; set; } = string.Empty;
+
+ /// <summary>
+ /// Gets or sets the identifier of the target stream.
+ /// Can be either a numeric ID or a string name.
+ /// </summary>
+ public Identifier StreamId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the identifier of the target topic.
+ /// Can be either a numeric ID or a string name.
+ /// </summary>
+ public Identifier TopicId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the size of the receive buffer in bytes.
+ /// Default is 4096 bytes (4 KB).
+ /// </summary>
+ public int ReceiveBufferSize { get; set; } = 4096;
+
+ /// <summary>
+ /// Gets or sets the size of the send buffer in bytes.
+ /// Default is 4096 bytes (4 KB).
+ /// </summary>
+ public int SendBufferSize { get; set; } = 4096;
+
+ /// <summary>
+ /// Gets or sets the partitioning strategy for messages.
+ /// Determines how messages are distributed across topic partitions.
+ /// Default is <see cref="Partitioning.None()" /> (balanced partitioning).
+ /// </summary>
+ public Partitioning Partitioning { get; set; } = Partitioning.None();
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to automatically create the stream if it doesn't exist.
+ /// When true, requires <see cref="StreamName" /> to be set.
+ /// </summary>
+ public bool CreateStream { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name to use when creating the stream.
+ /// Required when <see cref="CreateStream" /> is true.
+ /// </summary>
+ public string? StreamName { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether to automatically create the topic if it doesn't exist.
+ /// When true, requires <see cref="TopicName" /> to be set.
+ /// </summary>
+ public bool CreateTopic { get; set; }
+
+ /// <summary>
+ /// Gets or sets the name to use when creating the topic.
+ /// Required when <see cref="CreateTopic" /> is true.
+ /// </summary>
+ public string? TopicName { get; set; }
+
+ /// <summary>
+ /// Gets or sets the message encryptor for encrypting message payloads.
+ /// When set, all message payloads will be encrypted before sending.
+ /// </summary>
+ public IMessageEncryptor? MessageEncryptor { get; set; } = null;
+
+ /// <summary>
+ /// Gets or sets the logger factory for diagnostic logging.
+ /// When set, enables detailed logging of publisher operations.
+ /// </summary>
+ public ILoggerFactory? LoggerFactory { get; set; } = null;
+
+ /// <summary>
+ /// Gets or sets the number of partitions to create when creating the topic.
+ /// Only used when <see cref="CreateTopic" /> is true.
+ /// </summary>
+ public uint TopicPartitionsCount { get; set; }
+
+ /// <summary>
+ /// Gets or sets the compression algorithm to use for the topic.
+ /// Only used when <see cref="CreateTopic" /> is true.
+ /// </summary>
+ public CompressionAlgorithm TopicCompressionAlgorithm { get; set; }
+
+ /// <summary>
+ /// Gets or sets the replication factor for the topic.
+ /// Determines how many replicas of each partition are maintained.
+ /// Only used when <see cref="CreateTopic" /> is true.
+ /// </summary>
+ public byte? TopicReplicationFactor { get; set; }
+
+ /// <summary>
+ /// Gets or sets the message expiry time in seconds (0 for no expiry).
+ /// Messages older than this will be automatically deleted.
+ /// Only used when <see cref="CreateTopic" /> is true.
+ /// </summary>
+ public ulong TopicMessageExpiry { get; set; }
+
+ /// <summary>
+ /// Gets or sets the maximum size of the topic in bytes (0 for unlimited).
+ /// When the topic reaches this size, oldest messages will be deleted.
+ /// Only used when <see cref="CreateTopic" /> is true.
+ /// </summary>
+ public ulong TopicMaxTopicSize { get; set; }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether background sending is enabled.
+ /// When enabled, messages are queued and sent asynchronously in batches.
+ /// Default is false (synchronous sending).
+ /// </summary>
+ public bool EnableBackgroundSending { get; set; } = false;
+
+ /// <summary>
+ /// Gets or sets the capacity of the background message queue.
+ /// When the queue is full, new messages will block until space becomes available.
+ /// Only used when <see cref="EnableBackgroundSending" /> is true.
+ /// Default is 10,000 messages.
+ /// </summary>
+ public int BackgroundQueueCapacity { get; set; } = 10000;
+
+ /// <summary>
+ /// Gets or sets the number of messages to send in each batch.
+ /// Larger batches improve throughput but increase latency.
+ /// Only used when <see cref="EnableBackgroundSending" /> is true.
+ /// Default is 100 messages.
+ /// </summary>
+ public int BackgroundBatchSize { get; set; } = 100;
+
+ /// <summary>
+ /// Gets or sets the interval at which to flush pending messages.
+ /// Messages are sent either when the batch size is reached or this interval expires.
+ /// Only used when <see cref="EnableBackgroundSending" /> is true.
+ /// Default is 100 milliseconds.
+ /// </summary>
+ public TimeSpan BackgroundFlushInterval { get; set; } = TimeSpan.FromMilliseconds(100);
+
+ /// <summary>
+ /// Gets or sets the timeout to wait for the background processor to complete during disposal.
+ /// Only used when <see cref="EnableBackgroundSending" /> is true.
+ /// Default is 5 seconds.
+ /// </summary>
+ public TimeSpan BackgroundDisposalTimeout { get; set; } = TimeSpan.FromSeconds(5);
+
+ /// <summary>
+ /// Gets or sets a value indicating whether retry is enabled for failed sends.
+ /// When enabled, failed send operations will be retried according to the retry policy.
+ /// Default is true.
+ /// </summary>
+ public bool EnableRetry { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the maximum number of retry attempts.
+ /// After this many failed attempts, the operation will fail permanently.
+ /// Only used when <see cref="EnableRetry" /> is true.
+ /// Default is 3 attempts.
+ /// </summary>
+ public int MaxRetryAttempts { get; set; } = 3;
+
+ /// <summary>
+ /// Gets or sets the initial delay before the first retry.
+ /// Subsequent retries use exponential backoff based on <see cref="RetryBackoffMultiplier" />.
+ /// Only used when <see cref="EnableRetry" /> is true.
+ /// Default is 100 milliseconds.
+ /// </summary>
+ public TimeSpan InitialRetryDelay { get; set; } = TimeSpan.FromMilliseconds(100);
+
+ /// <summary>
+ /// Gets or sets the maximum delay between retries.
+ /// Prevents exponential backoff from growing indefinitely.
+ /// Only used when <see cref="EnableRetry" /> is true.
+ /// Default is 10 seconds.
+ /// </summary>
+ public TimeSpan MaxRetryDelay { get; set; } = TimeSpan.FromSeconds(10);
+
+ /// <summary>
+ /// Gets or sets the multiplier for exponential backoff retry delays.
+ /// Each retry delay is calculated as: min(InitialRetryDelay * (multiplier ^ attempt), MaxRetryDelay).
+ /// Only used when <see cref="EnableRetry" /> is true.
+ /// Default is 2.0 (doubles the delay each time).
+ /// </summary>
+ public double RetryBackoffMultiplier { get; set; } = 2.0;
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherOfT.cs b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherOfT.cs
new file mode 100644
index 0000000..7eeb594
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/IggyPublisherOfT.cs
@@ -0,0 +1,116 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Headers;
+using Apache.Iggy.IggyClient;
+using Apache.Iggy.Messages;
+using Microsoft.Extensions.Logging;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Typed publisher that automatically serializes objects of type T to message payloads.
+/// Extends <see cref="IggyPublisher" /> with serialization capabilities.
+/// </summary>
+/// <typeparam name="T">The type to serialize to message payloads</typeparam>
+public class IggyPublisher<T> : IggyPublisher
+{
+ private readonly IggyPublisherConfig<T> _typedConfig;
+ private readonly ILogger<IggyPublisher<T>> _typedLogger;
+
+ /// <summary>
+ /// Initializes a new instance of the typed <see cref="IggyPublisher{T}" /> class
+ /// </summary>
+ /// <param name="client">The Iggy client for server communication</param>
+ /// <param name="config">Typed publisher configuration including serializer</param>
+ /// <param name="logger">Logger instance for diagnostic output</param>
+ public IggyPublisher(IIggyClient client, IggyPublisherConfig<T> config, ILogger<IggyPublisher<T>> logger) : base(
+ client, config, logger)
+ {
+ _typedConfig = config;
+ _typedLogger = logger;
+ }
+
+ /// <summary>
+ /// Serializes and sends a single object as a message
+ /// </summary>
+ /// <param name="data">The object to serialize and send</param>
+ /// <param name="messageId">Optional message ID. If null, a new GUID will be generated</param>
+ /// <param name="userHeaders">Optional user headers to attach to the message</param>
+ /// <param name="ct">Cancellation token</param>
+ public async Task SendAsync(T data, Guid? messageId = null,
+ Dictionary<HeaderKey, HeaderValue>? userHeaders = null, CancellationToken ct = default)
+ {
+ var message = CreateMessage(data, messageId, userHeaders);
+ await SendMessages([message], ct);
+ }
+
+ /// <summary>
+ /// Serializes and sends a collection of objects as messages
+ /// </summary>
+ /// <param name="data">The collection of objects to serialize and send</param>
+ /// <param name="ct">Cancellation token</param>
+ public async Task SendAsync(IEnumerable<T> data, CancellationToken ct = default)
+ {
+ var messages = data.Select(item => CreateMessage(item, null, null)).ToList();
+ await SendMessages(messages, ct);
+ }
+
+ /// <summary>
+ /// Serializes and sends a collection of objects with custom message configuration
+ /// </summary>
+ /// <param name="items">The collection of items to send, each with optional message ID and headers</param>
+ /// <param name="ct">Cancellation token</param>
+ public async Task SendAsync(IEnumerable<(T data, Guid? messageId, Dictionary<HeaderKey, HeaderValue>? userHeaders)> items,
+ CancellationToken ct = default)
+ {
+ var messages = items.Select(item => CreateMessage(item.data, item.messageId, item.userHeaders)).ToList();
+ await SendMessages(messages, ct);
+ }
+
+ /// <summary>
+ /// Serializes an object using the configured serializer
+ /// </summary>
+ /// <param name="data">The object to serialize</param>
+ /// <returns>The serialized byte array</returns>
+ public byte[] Serialize(T data)
+ {
+ return _typedConfig.Serializer.Serialize(data);
+ }
+
+ /// <summary>
+ /// Creates a message from an object by serializing it
+ /// </summary>
+ /// <param name="data">The object to serialize</param>
+ /// <param name="messageId">Optional message ID</param>
+ /// <param name="userHeaders">Optional user headers</param>
+ /// <returns>A new message with the serialized payload</returns>
+ private Message CreateMessage(T data, Guid? messageId, Dictionary<HeaderKey, HeaderValue>? userHeaders)
+ {
+ try
+ {
+ var payload = Serialize(data);
+ var id = messageId ?? Guid.NewGuid();
+ return new Message(id, payload, userHeaders);
+ }
+ catch (Exception ex)
+ {
+ _typedLogger.LogError(ex, "Failed to serialize message of type {Type}", typeof(T).Name);
+ throw;
+ }
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/MessageBatchFailedEventArgs.cs b/foreign/csharp/Iggy_SDK/Publishers/MessageBatchFailedEventArgs.cs
new file mode 100644
index 0000000..4d12b2f
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/MessageBatchFailedEventArgs.cs
@@ -0,0 +1,60 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+using Apache.Iggy.Messages;
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Event arguments for failed message batch send events.
+/// </summary>
+public class MessageBatchFailedEventArgs : EventArgs
+{
+ /// <summary>
+ /// Gets the exception that caused the batch to fail.
+ /// </summary>
+ public Exception Exception { get; }
+
+ /// <summary>
+ /// Gets the array of messages that failed to send.
+ /// </summary>
+ public Message[] FailedMessages { get; }
+
+ /// <summary>
+ /// Gets the UTC timestamp when the failure occurred.
+ /// </summary>
+ public DateTime Timestamp { get; }
+
+ /// <summary>
+ /// Gets the number of retry attempts that were made before failing.
+ /// </summary>
+ public int AttemptedRetries { get; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageBatchFailedEventArgs" /> class.
+ /// </summary>
+ /// <param name="exception">The exception that caused the batch to fail.</param>
+ /// <param name="failedMessages">The array of messages that failed to send.</param>
+ /// <param name="attemptedRetries">The number of retry attempts that were made.</param>
+ public MessageBatchFailedEventArgs(Exception exception, Message[] failedMessages, int attemptedRetries = 0)
+ {
+ Exception = exception;
+ FailedMessages = failedMessages;
+ Timestamp = DateTime.UtcNow;
+ AttemptedRetries = attemptedRetries;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Publishers/PublisherErrorEventArgs.cs b/foreign/csharp/Iggy_SDK/Publishers/PublisherErrorEventArgs.cs
new file mode 100644
index 0000000..e904ddf
--- /dev/null
+++ b/foreign/csharp/Iggy_SDK/Publishers/PublisherErrorEventArgs.cs
@@ -0,0 +1,51 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+namespace Apache.Iggy.Publishers;
+
+/// <summary>
+/// Event arguments for publisher error events.
+/// </summary>
+public class PublisherErrorEventArgs : EventArgs
+{
+ /// <summary>
+ /// Gets the exception that occurred.
+ /// </summary>
+ public Exception Exception { get; }
+
+ /// <summary>
+ /// Gets a descriptive message about the error.
+ /// </summary>
+ public string Message { get; }
+
+ /// <summary>
+ /// Gets the UTC timestamp when the error occurred.
+ /// </summary>
+ public DateTime Timestamp { get; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PublisherErrorEventArgs" /> class.
+ /// </summary>
+ /// <param name="exception">The exception that occurred.</param>
+ /// <param name="message">A descriptive message about the error.</param>
+ public PublisherErrorEventArgs(Exception exception, string message)
+ {
+ Exception = exception;
+ Message = message;
+ Timestamp = DateTime.UtcNow;
+ }
+}
diff --git a/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs b/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
index d79d71e..7f98ae7 100644
--- a/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
+++ b/foreign/csharp/Iggy_SDK/Utils/TcpMessageStreamHelpers.cs
@@ -45,14 +45,22 @@
internal static int CalculateMessageBytesCount(IList<Message> messages)
{
- return messages switch
+ var bytesCount = 0;
+ foreach (var message in messages)
{
- Message[] messagesArray => CalculateMessageBytesCountArray(messagesArray),
- List<Message> messagesList => CalculateMessageBytesCountList(messagesList),
- _ => messages.Sum(msg => 16 + 56 + msg.Payload.Length + 4 +
- (msg.UserHeaders?.Sum(header =>
- 4 + header.Key.Value.Length + 1 + 4 + header.Value.Value.Length) ?? 0))
- };
+ bytesCount += 16 + 56 + message.Payload.Length;
+ if (message.UserHeaders is null)
+ {
+ continue;
+ }
+
+ foreach (var header in message.UserHeaders)
+ {
+ bytesCount += 4 + header.Key.Value.Length + 1 + 4 + header.Value.Value.Length;
+ }
+ }
+
+ return bytesCount;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -73,57 +81,4 @@
return bytes.ToArray();
}
-
- private static int CalculateMessageBytesCountArray(Message[] messages)
- {
- ref var start = ref MemoryMarshal.GetArrayDataReference(messages);
- ref var end = ref Unsafe.Add(ref start, messages.Length);
- var msgBytesSum = 0;
- while (Unsafe.IsAddressLessThan(ref start, ref end))
- {
- if (start.UserHeaders is not null)
- {
- msgBytesSum += start.Payload.Length + 16 + 56;
- foreach (var (headerKey, headerValue) in start.UserHeaders)
- {
- msgBytesSum += 4 + headerKey.Value.Length + 1 + 4 + headerValue.Value.Length;
- }
- }
- else
- {
- msgBytesSum += start.Payload.Length + 16 + 56;
- }
-
- start = ref Unsafe.Add(ref start, 1);
- }
-
- return msgBytesSum;
- }
-
- private static int CalculateMessageBytesCountList(List<Message> messages)
- {
- Span<Message> messagesSpan = CollectionsMarshal.AsSpan(messages);
- ref var start = ref MemoryMarshal.GetReference(messagesSpan);
- ref var end = ref Unsafe.Add(ref start, messagesSpan.Length);
- var msgBytesSum = 0;
- while (Unsafe.IsAddressLessThan(ref start, ref end))
- {
- if (start.UserHeaders is not null)
- {
- msgBytesSum += start.Payload.Length + 16 + 56;
- foreach (var (headerKey, headerValue) in start.UserHeaders)
- {
- msgBytesSum += 4 + headerKey.Value.Length + 1 + 4 + headerValue.Value.Length;
- }
- }
- else
- {
- msgBytesSum += start.Payload.Length + 16 + 56;
- }
-
- start = ref Unsafe.Add(ref start, 1);
- }
-
- return msgBytesSum;
- }
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK_Tests/ContractTests/TcpContract.cs b/foreign/csharp/Iggy_SDK_Tests/ContractTests/TcpContract.cs
index 8cf99bc..c96d93e 100644
--- a/foreign/csharp/Iggy_SDK_Tests/ContractTests/TcpContract.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/ContractTests/TcpContract.cs
@@ -535,7 +535,7 @@
_ => throw new ArgumentOutOfRangeException()
}, request.PollingStrategy.Kind);
Assert.Equal(request.PollingStrategy.Value, BitConverter.ToUInt64(result[24..32]));
- Assert.Equal(request.Count, BitConverter.ToInt32(result[32..36]));
+ Assert.Equal(request.Count, BitConverter.ToUInt32(result[32..36]));
Assert.Equal(request.AutoCommit, result[36] switch
{
0 => false,
@@ -1006,4 +1006,4 @@
bytes[i + position + 2] = topicId.Value[i];
}
}
-}
\ No newline at end of file
+}
diff --git a/foreign/csharp/Iggy_SDK_Tests/Utils/Messages/MessageFactory.cs b/foreign/csharp/Iggy_SDK_Tests/Utils/Messages/MessageFactory.cs
index 34a0038..dffe32a 100644
--- a/foreign/csharp/Iggy_SDK_Tests/Utils/Messages/MessageFactory.cs
+++ b/foreign/csharp/Iggy_SDK_Tests/Utils/Messages/MessageFactory.cs
@@ -226,7 +226,7 @@
{
return new MessageFetchRequest
{
- Count = Random.Shared.Next(1, 10),
+ Count = (uint)Random.Shared.Next(1, 10),
AutoCommit = true,
Consumer = Consumer.New(1),
PartitionId = (uint)Random.Shared.Next(1, 10),
@@ -241,7 +241,7 @@
{
return new MessageFetchRequest
{
- Count = count,
+ Count = (uint)count,
AutoCommit = true,
Consumer = Consumer.New(consumerId),
PartitionId = partitionId,
@@ -256,7 +256,7 @@
{
return new MessageFetchRequest
{
- Count = count,
+ Count = (uint)count,
AutoCommit = true,
Consumer = Consumer.Group(consumerGroupId),
PartitionId = partitionId,
@@ -325,4 +325,4 @@
{
public required int Id { get; set; }
public required string Text { get; set; }
-}
\ No newline at end of file
+}