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">&lt;Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"&gt;
+  &lt;TypePattern DisplayName="Non-reorderable types" Priority="99999999"&gt;
+    &lt;TypePattern.Match&gt;
+      &lt;Or&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Interface" /&gt;
+          &lt;Or&gt;
+            &lt;HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" /&gt;
+            &lt;HasAttribute Name="System.Runtime.InteropServices.ComImport" /&gt;
+          &lt;/Or&gt;
+        &lt;/And&gt;
+        &lt;Kind Is="Struct" /&gt;
+        &lt;HasAttribute Name="System.Runtime.InteropServices.StructLayoutAttribute" /&gt;
+        &lt;HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" /&gt;
+      &lt;/Or&gt;
+    &lt;/TypePattern.Match&gt;
+  &lt;/TypePattern&gt;
+
+  &lt;TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All"&gt;
+    &lt;TypePattern.Match&gt;
+      &lt;And&gt;
+        &lt;Kind Is="Class" /&gt;
+        &lt;HasMember&gt;
+          &lt;And&gt;
+            &lt;Kind Is="Method" /&gt;
+            &lt;HasAttribute Name="Xunit.FactAttribute" Inherited="True" /&gt;
+            &lt;HasAttribute Name="Xunit.TheoryAttribute" Inherited="True" /&gt;
+          &lt;/And&gt;
+        &lt;/HasMember&gt;
+      &lt;/And&gt;
+    &lt;/TypePattern.Match&gt;
+
+    &lt;Entry DisplayName="Fields"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Field" /&gt;
+          &lt;Not&gt;
+            &lt;Static /&gt;
+          &lt;/Not&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Readonly /&gt;
+        &lt;Name /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="Constructors"&gt;
+      &lt;Entry.Match&gt;
+        &lt;Kind Is="Constructor" /&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Static/&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="Teardown Methods"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Method" /&gt;
+          &lt;ImplementsInterface Name="System.IDisposable" /&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="All other members" /&gt;
+
+    &lt;Entry DisplayName="Test Methods" Priority="100"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Method" /&gt;
+          &lt;HasAttribute Name="Xunit.FactAttribute" Inherited="false" /&gt;
+          &lt;HasAttribute Name="Xunit.TheoryAttribute" Inherited="false" /&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Name /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+  &lt;/TypePattern&gt;
+
+  &lt;TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All"&gt;
+    &lt;TypePattern.Match&gt;
+      &lt;And&gt;
+        &lt;Kind Is="Class" /&gt;
+        &lt;Or&gt;
+          &lt;HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="true" /&gt;
+          &lt;HasAttribute Name="NUnit.Framework.TestFixtureSourceAttribute" Inherited="true" /&gt;
+          &lt;HasMember&gt;
+            &lt;And&gt;
+              &lt;Kind Is="Method" /&gt;
+              &lt;HasAttribute Name="NUnit.Framework.TestAttribute" Inherited="false" /&gt;
+              &lt;HasAttribute Name="NUnit.Framework.TestCaseAttribute" Inherited="false" /&gt;
+              &lt;HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" Inherited="false" /&gt;
+            &lt;/And&gt;
+          &lt;/HasMember&gt;
+        &lt;/Or&gt;
+      &lt;/And&gt;
+    &lt;/TypePattern.Match&gt;
+
+    &lt;Entry DisplayName="Setup/Teardown Methods"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Method" /&gt;
+          &lt;Or&gt;
+            &lt;HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="true" /&gt;
+            &lt;HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="true" /&gt;
+            &lt;HasAttribute Name="NUnit.Framework.TestFixtureSetUpAttribute" Inherited="true" /&gt;
+            &lt;HasAttribute Name="NUnit.Framework.TestFixtureTearDownAttribute" Inherited="true" /&gt;
+            &lt;HasAttribute Name="NUnit.Framework.OneTimeSetUpAttribute" Inherited="true" /&gt;
+            &lt;HasAttribute Name="NUnit.Framework.OneTimeTearDownAttribute" Inherited="true" /&gt;
+          &lt;/Or&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="All other members" /&gt;
+
+    &lt;Entry DisplayName="Test Methods" Priority="100"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Method" /&gt;
+          &lt;HasAttribute Name="NUnit.Framework.TestAttribute" Inherited="false" /&gt;
+          &lt;HasAttribute Name="NUnit.Framework.TestCaseAttribute" Inherited="false" /&gt;
+          &lt;HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" Inherited="false" /&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Name /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+  &lt;/TypePattern&gt;
+
+  &lt;TypePattern DisplayName="Default Pattern"&gt;
+    &lt;Entry DisplayName="Public Delegates" Priority="100"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Access Is="Public" /&gt;
+          &lt;Kind Is="Delegate" /&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Name /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="Public Enums" Priority="100"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Access Is="Public" /&gt;
+          &lt;Kind Is="Enum" /&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Name /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="Static Fields and Constants"&gt;
+      &lt;Entry.Match&gt;
+        &lt;Or&gt;
+          &lt;Kind Is="Constant" /&gt;
+          &lt;And&gt;
+            &lt;Kind Is="Field" /&gt;
+            &lt;Static /&gt;
+          &lt;/And&gt;
+        &lt;/Or&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Kind&gt;
+          &lt;Kind.Order&gt;
+            &lt;DeclarationKind&gt;Constant&lt;/DeclarationKind&gt;
+            &lt;DeclarationKind&gt;Field&lt;/DeclarationKind&gt;
+          &lt;/Kind.Order&gt;
+        &lt;/Kind&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="Fields"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Field" /&gt;
+          &lt;Not&gt;
+            &lt;Static /&gt;
+          &lt;/Not&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Readonly /&gt;
+        &lt;Name /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+      &lt;Entry DisplayName="Properties, Indexers"&gt;
+          &lt;Entry.Match&gt;
+              &lt;Or&gt;
+                  &lt;Kind Is="Property" /&gt;
+                  &lt;Kind Is="Indexer" /&gt;
+              &lt;/Or&gt;
+          &lt;/Entry.Match&gt;
+      &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="Constructors"&gt;
+      &lt;Entry.Match&gt;
+        &lt;Kind Is="Constructor" /&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;Static/&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+      &lt;Entry DisplayName="Interface Implementations" Priority="100"&gt;
+      &lt;Entry.Match&gt;
+        &lt;And&gt;
+          &lt;Kind Is="Member" /&gt;
+          &lt;ImplementsInterface /&gt;
+        &lt;/And&gt;
+      &lt;/Entry.Match&gt;
+
+      &lt;Entry.SortBy&gt;
+        &lt;ImplementsInterface Immediate="true" /&gt;
+      &lt;/Entry.SortBy&gt;
+    &lt;/Entry&gt;
+
+    &lt;Entry DisplayName="All other members" /&gt;
+
+    &lt;Entry DisplayName="Nested Types"&gt;
+      &lt;Entry.Match&gt;
+        &lt;Kind Is="Type" /&gt;
+      &lt;/Entry.Match&gt;
+    &lt;/Entry&gt;
+  &lt;/TypePattern&gt;
+&lt;/Patterns&gt;
+</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
+}