diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..3c8af84
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,254 @@
+# http://EditorConfig.org
+
+root = true
+
+# Default settings -------------------------------------------------------------
+
+[*]
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+
+# C# files ---------------------------------------------------------------------
+
+[*.cs]
+
+# .NET code style settings -----------------------------------------------------
+
+## "This." and "Me." qualifiers
+
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+## Language keywords instead of framework type names for type references
+
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+
+## Modifier preferences
+
+dotnet_style_require_accessibility_modifiers = always:suggestion
+dotnet_style_readonly_field = true:warning
+
+## Parentheses preferences
+
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
+
+## Expression-level preferences
+
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:warning
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = false:suggestion
+
+## Null-checking preferences
+
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:warning
+
+## Formatting conventions
+
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = true
+
+# C# code style settings -------------------------------------------------------
+
+## Using statements
+
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+
+## Expression-bodied members
+
+csharp_style_expression_bodied_methods = unset
+csharp_style_expression_bodied_constructors = false:suggestion
+csharp_style_expression_bodied_operators = false:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_accessors = true:suggestion
+
+## Pattern matching
+
+csharp_style_pattern_matching_over_is_with_cast_check = true:warning
+csharp_style_pattern_matching_over_as_with_null_check = true:warning
+csharp_style_prefer_switch_expression = true:suggestion
+
+## Inlined variable declarations
+
+csharp_style_inlined_variable_declaration = true:suggestion
+
+## Expression-level preferences
+
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_pattern_local_over_anonymous_function = true:suggestion
+
+## "Null" checking preferences
+
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = false:suggestion
+
+## Code block preferences
+
+csharp_prefer_braces = true:error
+
+## C# formatting settings
+
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:warning
+
+## Indentation options
+
+csharp_indent_case_contents = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = flush_left
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+
+## Spacing options
+
+csharp_space_after_cast = true
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_before_open_square_brackets = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+
+## Wrapping options
+
+csharp_preserve_single_line_statements = false
+csharp_preserve_single_line_blocks = true
+
+
+# Naming rules ------------------------------------------------------------------------------------
+# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions?view=vs-2019
+
+## Private fields
+
+dotnet_naming_rule.camel_case_for_private_fields.severity = warning
+dotnet_naming_rule.camel_case_for_private_fields.symbols = private_fields_symbols
+dotnet_naming_rule.camel_case_for_private_fields.style = private_fields_style
+
+dotnet_naming_symbols.private_fields_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_fields_symbols.applicable_accessibilities = private
+dotnet_naming_style.private_fields_style.required_prefix = _
+dotnet_naming_style.private_fields_style.capitalization = camel_case
+
+## Async methods
+
+dotnet_naming_rule.async_method_name.severity = suggestion
+dotnet_naming_rule.async_method_name.symbols = async_method_name_symbols
+dotnet_naming_rule.async_method_name.style = async_method_name_style
+
+dotnet_naming_symbols.async_method_name_symbols.applicable_kinds = method,delegate
+dotnet_naming_symbols.async_method_name_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.async_method_name_symbols.required_modifiers = async
+
+dotnet_naming_style.async_method_name_style.required_suffix = Async
+dotnet_naming_style.async_method_name_style.capitalization = pascal_case
+
+## Async local functions
+
+dotnet_naming_rule.async_local_function_name.severity = warning
+dotnet_naming_rule.async_local_function_name.symbols = async_local_function_name_symbols
+dotnet_naming_rule.async_local_function_name.style = async_local_function_name_style
+
+dotnet_naming_symbols.async_local_function_name_symbols.applicable_kinds = local_function
+dotnet_naming_symbols.async_local_function_name_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.async_local_function_name_symbols.required_modifiers = async
+
+dotnet_naming_style.async_local_function_name_style.required_suffix = Async
+dotnet_naming_style.async_local_function_name_style.capitalization = camel_case
+
+## Sync local functions
+
+dotnet_naming_rule.sync_local_function_name.severity = warning
+dotnet_naming_rule.sync_local_function_name.symbols = sync_local_function_name_symbols
+dotnet_naming_rule.sync_local_function_name.style = sync_local_function_name_style
+
+dotnet_naming_symbols.sync_local_function_name_symbols.applicable_kinds = local_function
+dotnet_naming_symbols.sync_local_function_name_symbols.applicable_accessibilities = *
+
+dotnet_naming_style.sync_local_function_name_style.capitalization = camel_case
+
+## Internal data
+
+dotnet_naming_rule.pascal_case_for_internal_data.severity = suggestion
+dotnet_naming_rule.pascal_case_for_internal_data.symbols = internal_data_symbols
+dotnet_naming_rule.pascal_case_for_internal_data.style = internal_data_style
+
+dotnet_naming_symbols.internal_data_symbols.applicable_kinds = field, property
+dotnet_naming_symbols.internal_data_symbols.applicable_accessibilities = internal
+dotnet_naming_style.internal_data_style.capitalization = pascal_case
+
+
+# Analyzers ----------------------------------------------------------------------
+
+dotnet_diagnostic.ca1032.severity = none # Default exception constructors.
+dotnet_diagnostic.ca1062.severity = none # Validate arguments of public methods.
+dotnet_diagnostic.ca1303.severity = none # Do not pass literals as localized parameters.
+dotnet_diagnostic.ca1308.severity = none # Normalize strings to uppercase.
+dotnet_diagnostic.ca1707.severity = none # Identifiers should not contain underscores.
+dotnet_diagnostic.ca1710.severity = none # Name of type must end with 'Collection'.
+dotnet_diagnostic.ca1716.severity = none # Identifiers should not match keywords.
+dotnet_diagnostic.ca1724.severity = none # Type names should not match namespaces.
+dotnet_diagnostic.ca1812.severity = none # Class is an internal class that is apparently never instantiated.
+dotnet_diagnostic.ca2007.severity = none # Consider calling ConfigureAwait on the awaited task.
+dotnet_diagnostic.ca2016.severity = warning # Consider calling ConfigureAwait on the awaited task.
+dotnet_diagnostic.ca5377.severity = none # Use Container Level Access Policy.
+dotnet_diagnostic.ca1805.severity = none # Do not initialize unnecessarily.
+dotnet_diagnostic.cs8509.severity = error # The switch expression does not handle all possible inputs.
+dotnet_diagnostic.cs8524.severity = none # The switch expression does not handle some values of its input type.
+
+
+# Visual Studio - Analyzer settings ---------------------------------------------------------
+
+dotnet_diagnostic.ide0022.severity = none # Use expression body for methods
+dotnet_diagnostic.ide0052.severity = warning # Remove unused members
+# Rider/ReShaper - Inspections settings -----------------------------------------------------
+
+resharper_csharp_max_line_length = 180
+resharper_c_sharp_warnings_cs8509_highlighting = none
+resharper_wrap_object_and_collection_initializer_style = chop_always
+resharper_csharp_wrap_after_declaration_lpar = true
+resharper_inconsistent_naming_highlighting = none
+
+# Solution-specific settings/overrides ------------------------------------------------------
+
+## TBD
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..e7144c3
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,131 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - v*
+ paths-ignore:
+ - '*.md'
+ - 'docs/**'
+ - '.github/workflows/purge-packages.yml'
+ pull_request:
+ paths-ignore:
+ - '*.md'
+ - 'docs/**'
+ - '.github/workflows/purge-packages.yml'
+
+jobs:
+ init:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.resolve-version.outputs.version }}
+ publish-nuget-org: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
+ publish-github: ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/tags/v') }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Install dotnet-gitversion
+ run: |
+ echo ":/github/home/.dotnet/tools" >> $GITHUB_PATH
+ dotnet tool install -g GitVersion.Tool --version "5.12.0" --ignore-failed-sources
+ shell: bash
+ - name: Resolve Version
+ id: resolve-version
+ shell: pwsh
+ working-directory: ./
+ run: |
+ $rawVersion = dotnet-gitversion | ConvertFrom-Json
+ Write-Output $rawVersion
+ $semVer = $rawVersion.SemVer
+ Write-Output "Resolved version $semVer"
+ Write-Output "version=$semVer" >> $env:GITHUB_OUTPUT
+ Write-Output "Version = $semVer" >> $env:GITHUB_STEP_SUMMARY
+
+ build:
+ runs-on: ubuntu-latest
+ needs: init
+ env:
+ Configuration: Release
+ TreatWarningsAsErrors: true
+ Version: ${{ needs.init.outputs.version }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Restore
+ run: dotnet restore
+ - name: Check Format
+ run: dotnet format --no-restore --verify-no-changes
+ - name: Build
+ run: dotnet build --no-restore
+ - name: Test
+ run: dotnet test --no-restore
+ working-directory: ./tests/Tests
+ - name: Pack
+ run: dotnet pack --no-build -o nugets/
+ - name: Validate NuGets
+ run: |
+ echo ':/github/home/.dotnet/tools' >> $env:GITHUB_PATH
+ dotnet tool install -g dotnet-validate --version '0.0.1-preview.304' --ignore-failed-sources
+ Get-ChildItem ./nugets -Filter '*.nupkg' | ForEach-Object { dotnet-validate package local $_ }
+ shell: pwsh
+ - name: Upload Nugets as Artifacts
+ uses: actions/upload-artifact@v4
+ if: ${{ needs.init.outputs.publish-nuget-org == 'true' || needs.init.outputs.publish-github == 'true'}}
+ with:
+ name: nugets
+ path: nugets
+
+ test-with-azure-infra:
+ runs-on: ubuntu-latest
+ environment: azure
+ permissions:
+ contents: read
+ id-token: write
+ env:
+ Configuration: Release
+ TreatWarningsAsErrors: true
+ steps:
+ - uses: actions/checkout@v4
+ - uses: azure/login@v2
+ with:
+ tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ - run: dotnet test
+ env:
+ SPOTFLOW_USE_AZURE: true
+ AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
+ AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
+ AZURE_RESOURCE_GROUP_NAME: ${{ secrets.AZURE_RESOURCE_GROUP_NAME }}
+ AZURE_STORAGE_ACCOUNT_NAME: ${{ secrets.AZURE_STORAGE_ACCOUNT_NAME }}
+ AZURE_SERVICE_BUS_NAMESPACE_NAME: ${{ secrets.AZURE_SERVICE_BUS_NAMESPACE_NAME }}
+ AZURE_KEY_VAULT_NAME: ${{ secrets.AZURE_KEY_VAULT_NAME }}
+
+ publish-nuget-org:
+ runs-on: ubuntu-latest
+ needs: [init, build, test-with-azure-infra]
+ if: ${{ needs.init.outputs.publish-nuget-org == 'true' }}
+ environment: nuget-org
+ steps:
+ - name: Download Nugets
+ uses: actions/download-artifact@v4
+ with:
+ name: nugets
+ - name: Push Nugets
+ run: dotnet nuget push '*.nupkg' --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
+
+ publish-github:
+ runs-on: ubuntu-latest
+ needs: [init, build, test-with-azure-infra]
+ permissions:
+ packages: write
+ if: ${{ needs.init.outputs.publish-github == 'true' }}
+ steps:
+ - name: Download Nugets
+ uses: actions/download-artifact@v4
+ with:
+ name: nugets
+ - name: Push Nugets
+ run: dotnet nuget push '*.nupkg' --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/spotflow-io/index.json --skip-duplicate
\ No newline at end of file
diff --git a/.github/workflows/purge-packages.yml b/.github/workflows/purge-packages.yml
new file mode 100644
index 0000000..06d8e4c
--- /dev/null
+++ b/.github/workflows/purge-packages.yml
@@ -0,0 +1,56 @@
+name: Purge Packages
+
+on:
+ schedule:
+ - cron: "0 0 * * *"
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ paths:
+ - .github/workflows/purge-packages.yml
+
+permissions:
+ packages: write
+ contents: read
+
+jobs:
+ list-packages:
+ runs-on: ubuntu-latest
+ outputs:
+ nugets: ${{ steps.nugets.outputs.packages }}
+ permissions:
+ contents: read
+ packages: read
+ steps:
+ - id: nugets
+ run: |
+ all_packages=$(gh api '/orgs/${{ github.repository_owner }}/packages?package_type=nuget' -H 'Accept: application/vnd.github+json' -H 'X-GitHub-Api-Version: 2022-11-28')
+
+ echo $all_packages | jq
+
+ packages=$(echo $all_packages | jq -c '[.[].name]')
+
+ echo $packages | jq
+ echo $packages | jq >> $GITHUB_STEP_SUMMARY
+ echo "packages=$packages" >> $GITHUB_OUTPUT
+
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ purge-nugets:
+ runs-on: ubuntu-latest
+ needs: list-packages
+ permissions:
+ packages: write
+ strategy:
+ matrix:
+ package: ${{ fromJson(needs.list-packages.outputs.nugets) }}
+ steps:
+ - uses: actions/delete-package-versions@v5
+ with:
+ owner: ${{ github.repository_owner }}
+ package-name: ${{ matrix.package }}
+ package-type: nuget
+ min-versions-to-keep: 10
+ delete-only-pre-release-versions: true
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0228ea2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,262 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+.acrbuild
+
+*.ldb
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+coverage.json
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+#*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+/Configuration/DatabaseCredentials.txt
+/Backend/Datamole.Lely.Lssa/Automation.LegacyContentConverter/Configuration/DatabaseCredentials.txt
+/Datamole.Dsa/SystemTests/Credentials.json
+
+.ipynb_checkpoints/
+sln/Api/Properties/launchSettings.json
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..66095a7
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,3 @@
+# Contributor Code of Conduct
+
+This project accepts contributions from anyone who's intentions are to improve the project and help the community. We are committed to providing a friendly, safe and welcoming environment for all, regardless of background and level of experience.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b80074a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,73 @@
+# Contributing guidelines
+
+By contributing to `In-Memory Azure Test SDKs`, you declare that:
+
+* You are entitled to assign the copyright for the work, provided it is not owned by your employer or you have received a written copyright assignment.
+* You license your contribution under the same terms that apply to the rest of the `In-Memory Azure Test SDKs` project.
+* You pledge to follow the [Code of Conduct](./CODE_OF_CONDUCT.md).
+
+## Contribution process
+
+Please, always create an [Issue](https://github.com/spotflow-io/in-memory-azure-test-sdk/issues/new) before starting to work on a new feature or bug fix. This way, we can discuss the best approach and avoid duplicated or lost work. Without discussing the issue first, there is a risk that your PR will not be accepted because e.g.:
+
+* It does not fit the project's goals.
+* It is not implemented in the way that we would like to see.
+* It is already being worked on by someone else.
+
+### Commits & Pull Requests
+
+We do not put any specific requirements on individual commits. However, we expect that the Pull Request (PR) is a logical unit of work that is easily understandable & reviewable. The PRs should also contain expressive title and description.
+
+Few general rules are:
+
+* Do not mix multiple unrelated changes in a single PR.
+* Do not mix formatting changes with functional changes.
+* Do not mix refactoring with functional changes.
+* Do not create huge PRs that are hard to review. In case that your change is logically cohesive but still large, consider splitting it into multiple PRs.
+
+### Code style
+
+This project generally follows usual code style for .NET projects as described in [Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/). In situations where Guideline is not clear, applicable or strict adherence to the Guideline obviously causes more harm than good, deviating from the Guideline is acceptable.
+
+We use `dotnet-format` to format the code. Code formatting is checked in the CI pipeline and failing to pass the formatting check will result in a failed build.
+
+### Testing
+
+All new code must be covered by tests. Prefer adding new tests specific for the new code, but feel free to extend existing tests if it makes sense.
+
+### Documentation & changelog
+
+All new features and changes must be reflected in the documentation [README.md](./README.md) and/or [docs](./docs). Also, make sure to update the [CHANGELOG.md](./CHANGELOG.md) file with a brief description of the changes. The changelog follows the [Keep a Changelog](https://keepachangelog.com) format.
+
+## Architecture of in-memory clients
+
+#### General rules
+
+* No static state should be used in the in-memory clients.
+
+#### Client constructors
+
+* The in-memory clients should have constructors that are as similar to the real clients as possible.
+* In-general, the constructors should omit authentication-related parameters. If the parameters are needed for constructor signature to be unique, these parameters can be included, but no-op/dummy implementation of the related type must be provided. (e.g. [NoOpTokenCredential](./src/Spotflow.InMemory.Azure/Auth/NoOpTokenCredential.cs)).
+
+#### Supported client methods & properties
+
+* All supported methods must also support their async counterparts if they exist in the real client.
+* All supported methods must be truly async (currently, this is mostly ensured by calling `Task.Yield()` at the start of each method).
+* If a supported method accepts a parameter that is related to a unsupported feature, the parameter should be ignored and the should not fail because of it.
+* No supported method should not return dummy constant value. Instead, they should return a value that reflects the current state of the in-memory provider.
+
+#### Unsupported client methods & properties
+
+* All unsupported methods should be explicitly overwritten and throw `NotSupportedException` exception. This way, user is not exposed to confusing behavior coming from inherited real clients which are (by design) not properly configured.
+
+#### Thread-safety
+
+* Clients must be thread-safe.
+
+#### In-memory providers & related types
+
+* Root in-memory provider should be always public and should have a public parameterless constructor if possible.
+* Visibility of related types representing the current in-memory state should be determined by how the concepts that the types represent are usually managed in Azure:
+ * If the concept mostly managed via Azure management-plane, the related type should be public and should expose relevant properties and methods to manage the concept. E.g. [InMemoryEventHub](./src/Spotflow.InMemory.Azure.EventHubs/InMemoryEventHub.cs).
+ * If the concept mostly managed via Azure data-plane, the related type should be internal. E.g. [InMemoryBlockBlob](./src/Spotflow.InMemory.Azure.Storage/Blobs/Internals/InMemoryBlockBlob.cs).
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..7326691
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,70 @@
+
+
+
+ Spotflow;Azure;Testing;Mocks;Fakes
+ PackageIcon.png
+ MIT
+ true
+ true
+ embedded
+ true
+ true
+ true
+ snupkg
+
+
+
+
+
+
+
+
+
+ net8.0
+ latest
+ enable
+ strict
+ enable
+ false
+ false
+
+
+
+
+
+ true
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..96dfcdd
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/GitVersion.yml b/GitVersion.yml
new file mode 100644
index 0000000..e91f8b2
--- /dev/null
+++ b/GitVersion.yml
@@ -0,0 +1,7 @@
+build-metadata-padding: 4
+mode: ContinuousDeployment
+branches:
+ main:
+ tag: beta
+ pull-request:
+ tag: alpha
\ No newline at end of file
diff --git a/HeroImage.jpg b/HeroImage.jpg
new file mode 100644
index 0000000..43d04a3
Binary files /dev/null and b/HeroImage.jpg differ
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..eeebf9d
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) 2024 Datamole
+
+Copyright (c) 2024 Spotflow
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/PackageIcon.png b/PackageIcon.png
new file mode 100644
index 0000000..73f83ee
Binary files /dev/null and b/PackageIcon.png differ
diff --git a/README.md b/README.md
index 50cf19e..1142b06 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,112 @@
-# in-memory-azure-test-sdk
-Drop-in fakes of Azure .NET SDKs to make your test blazing-fast and reliable.
+![](HeroImage.jpg)
+
+
Azure In-Memory SDKs for Testing
+
Drop-in fakes of Azure .NET SDKs to make your test blazing-fast and reliable.
+
+![CI status](https://github.com/spotflow-io/in-memory-azure-test-sdk/actions/workflows/ci.yml/badge.svg?branch=main)
+
+## Supported SDKs
+
+| Package | Relevant Azure SDK | NuGet |
+| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| [**Spotflow.InMemory.Azure.Storage**](./docs/storage.md) | [Azure.Storage.Blobs](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/storage.blobs-readme), [Azure.Data.Tables](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/data.tables-readme) | [![NuGet](https://img.shields.io/nuget/v/Spotflow.InMemory.Azure.Storage.svg)](https://www.nuget.org/packages/Spotflow.InMemory.Azure.Storage) |
+| [**Spotflow.InMemory.Azure.Storage.FluentAssertions**](./docs/storage.md) | | [![NuGet](https://img.shields.io/nuget/v/Spotflow.InMemory.Azure.Storage.FluentAssertions.svg)](https://www.nuget.org/packages/Spotflow.InMemory.Azure.Storage.FluentAssertions) |
+| [**Spotflow.InMemory.Azure.EventHubs**](./docs/event-hubs.md) | [Azure.Messaging.EventHubs](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/event-hubs) | [![NuGet](https://img.shields.io/nuget/v/Spotflow.InMemory.Azure.EventHubs.svg)](https://www.nuget.org/packages/Spotflow.InMemory.Azure.EventHubs) |
+| [**Spotflow.InMemory.Azure.ServiceBus**](./docs/service-bus.md) | [Azure.Messaging.ServiceBus](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/microsoft.servicebus-readme) | [![NuGet](https://img.shields.io/nuget/v/Spotflow.InMemory.Azure.ServiceBus.svg)](https://www.nuget.org/packages/Spotflow.InMemory.Azure.ServiceBus) |
+| [**Spotflow.InMemory.Azure.ServiceBus.FluentAssertions**](./docs/service-bus.md) | | [![NuGet](https://img.shields.io/nuget/v/Spotflow.InMemory.Azure.ServiceBus.FluentAssertions.svg)](https://www.nuget.org/packages/Spotflow.InMemory.Azure.ServiceBus.FluentAssertions) |
+| [**Spotflow.InMemory.Azure.KeyVault**](./docs/key-vault.md) | [Azure.Security.KeyVault.Secrets](https://learn.microsoft.com/en-us/dotnet/api/overview/azure/security.keyvault.secrets-readme) | [![NuGet](https://img.shields.io/nuget/v/Spotflow.InMemory.Azure.KeyVault.svg)](https://www.nuget.org/packages/Spotflow.InMemory.Azure.KeyVault) |
+
+## Example
+
+See how the in-memory Azure SDKs can be used in your code, for example with Azure Storage:
+
+![Design](./docs/images/intro.excalidraw.svg)
+
+```csharp
+var storageAccount = new InMemoryStorageProvider().AddAccount();
+
+// The InMemoryBlobContainerClient inherits from BlobContainerClient (from the official SDK)
+// So it can be used as a drop-in replacement for the real BlobContainerClient in your tests
+var containerClient = InMemoryBlobContainerClient.FromAccount(storageAccount, "test-container");
+
+// From now on, you can use the BlobContainerClient methods as you're used to:
+containerClient.Create();
+
+await containerClient.UploadBlobAsync("my-blob", BinaryData.FromString("Hello World!"));
+
+// The `containerClient` can now be used in your code as if it was a real BlobContainerClient:
+
+await PrintBlobAsync(containerClient);
+
+async Task PrintBlobAsync(BlobContainerClient container)
+{
+ var blob = container.GetBlobClient("my-blob");
+
+ var response = await blob.DownloadContentAsync();
+
+ Console.WriteLine(response.Value.Content.ToString());
+ // Output: Hello World!
+}
+```
+
+This design allows you to create a factory for SDK clients with two implementations: one that provides the official Azure SDK clients and another that provides in-memory clients.
+By selecting the appropriate factory, you can use the real implementation in your production code and the in-memory implementation in your tests.
+
+You can learn how we recommend to use this library in [the documentations for each SDK](#supported-sdks).
+
+## Key Features
+
+- **Drop-in Replacement** of the official Azure SDKs.
+- **Blazing-fast** thanks to the in-memory implementation.
+- **No external dependencies**, not even Docker.
+- **Fault Injection**: build resilient code thanks to simulated Azure outages in your tests.
+- **Delay Injection**: Examine behavior of your system under pressure of slowed-down operations.
+- **`TimeProvider` Support**: avoid flaky tests thanks to the time abstraction.
+- **Fluent Assertions** to conveniently test common scenarios.
+- **Customizable**: you can easily extend the functionality of the in-memory providers via [before and after hooks](./docs/hooks.md).
+
+## Why Should I Use It?
+
+There's been a lot written on why to prefer fakes over mocks in tests.
+Mocks are test-doubles that return pre-programmed responses to method calls.
+This can tightly couple your tests to implementation details, making them brittle and hard to maintain.
+Fakes, on the other hand, are lightweight implementations of real services that can seamlessly integrate into your tests.
+Using real services in tests is another approach, which is reasonable in many cases but can result in tests that are slow and harder to manage.
+
+**One major drawback of fakes is the initial effort required to create them.
+We have solved this problem by implementing them for you.**
+This way, you can use the same interfaces and methods as in the real SDKs, but with the benefits of in-memory implementation.
+
+## How It Works
+
+The Azure SDKs are [designed](https://learn.microsoft.com/en-us/dotnet/azure/sdk/unit-testing-mocking?tabs=csharp) for inheritance-based testability:
+
+- Important methods are `virtual`.
+- There are parameterless protected constructor available for all clients.
+- There are static factories for creating most models.
+
+The in-memory clients (e.g. `InMemoryBlobContainerClient` or `InMemoryEventHubConsumerClient`) provided by this library are inheriting the Azure SDK clients so that they can be injected to any code that expected the actual Azure SDK clients (the `BlobContainerClient` or `EventHubConsumerClient` the previous example). The tested code can therefore depend directly on Azure SDK clients and only abstract away creation of these clients. This removes the need to design and implement custom client interfaces.
+
+The in-memory clients have similar constructors as real clients but they all also require a so-called in-memory provider (e.g. `InMemoryStorageProvider` or `InMemoryEventHubProvider`). The in-memory providers emulate the functionality of the actual services for which the SDK clients are created for (e.g. Azure Storage or Azure Event Hubs). The providers allows to read, change and assert the internal state during testing. For most Azure SDK clients, the in-memory providers are exposing corresponding types representing actual state. For example for `InMemoryBlobContainerClient: BlobContainerClient`, there is `InMemoryBlobContainer` type exposed by the provider. The important difference is that the `InMemoryBlobContainer` is representing the actual state (in this case an existing Azure Storage Blob Container) while `InMemoryBlobContainerClient` might be representing container that does not yet exist.
+
+## Maintainers
+
+- [Tomáš Pajurek](https://github.com/tomas-pajurek) (Spotflow)
+- [David Nepožitek](https://github.com/DavidNepozitek) (Spotflow)
+
+## Contributing
+
+Please read our [Contributing Guidelines](./CONTRIBUTING.md) to learn how you can contribute to this project.
+
+## License
+
+This project is licensed under the [MIT license](./LICENSE.md).
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..6351e88
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,13 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+If a newly discovered vulnerability or security issue is discovered, we kindly ask our users and security researchers to disclose it privately and securely via the [GitHub Security Advisories (GHSA)](https://github.com/spotflow-io/in-memory-azure-test-sdk/security/advisories/new) feature on this repository. Please do not report vulnerabilities via GitHub issues or other public channels. Disclosing a vulnerability publicly might lead to a situation where a vulnerability is widely known, but no fix is yet available, thus harming other users.
+
+Alternatively, the report can be sent via email to `security@spotflow.io`. However, we prefer GHSA for security reasons.
+
+Ultimately, we will publish all vulnerabilities publicly and credit the reporter appropriately for the discovery, but only after a fix is available.
+
+## Bug Bounty Programs
+
+Currently, we are not running any bug bounty programs.
\ No newline at end of file
diff --git a/Spotflow.InMemory.Azure.sln b/Spotflow.InMemory.Azure.sln
new file mode 100644
index 0000000..68b0790
--- /dev/null
+++ b/Spotflow.InMemory.Azure.sln
@@ -0,0 +1,76 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34525.116
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2C6BE026-65BE-4D7A-8E4D-F8CF8E2C300E}"
+ ProjectSection(SolutionItems) = preProject
+ Directory.Build.props = Directory.Build.props
+ Directory.Packages.props = Directory.Packages.props
+ LICENSE.md = LICENSE.md
+ nuget.config = nuget.config
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure.EventHubs", "src\Spotflow.InMemory.Azure.EventHubs\Spotflow.InMemory.Azure.EventHubs.csproj", "{BA34B3BD-E616-4835-AEA8-6D6A97E637BC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure", "src\Spotflow.InMemory.Azure\Spotflow.InMemory.Azure.csproj", "{5B3ECEB4-0E24-4040-9429-BA63333C6B66}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure.ServiceBus", "src\Spotflow.InMemory.Azure.ServiceBus\Spotflow.InMemory.Azure.ServiceBus.csproj", "{1C052B5D-CA36-4AF0-89B3-C563F62D1B6C}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure.Storage", "src\Spotflow.InMemory.Azure.Storage\Spotflow.InMemory.Azure.Storage.csproj", "{DB49720B-C0E5-42BB-A9C6-77D3DC065D09}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "tests\Tests\Tests.csproj", "{BFF0A04C-5057-4A05-9122-CBF98C25DA33}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure.ServiceBus.FluentAssertions", "src\Spotflow.InMemory.Azure.ServiceBus.FluentAssertions\Spotflow.InMemory.Azure.ServiceBus.FluentAssertions.csproj", "{49091FD7-4936-42B3-9189-C18796B84269}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure.Storage.FluentAssertions", "src\Spotflow.InMemory.Azure.Storage.FluentAssertions\Spotflow.InMemory.Azure.Storage.FluentAssertions.csproj", "{5D749882-6633-415A-9DC3-5C7C208B29CB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spotflow.InMemory.Azure.KeyVault", "src\Spotflow.InMemory.Azure.KeyVault\Spotflow.InMemory.Azure.KeyVault.csproj", "{BFC359F7-8FE7-4ABC-B192-8E9DE43460A5}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {BA34B3BD-E616-4835-AEA8-6D6A97E637BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BA34B3BD-E616-4835-AEA8-6D6A97E637BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BA34B3BD-E616-4835-AEA8-6D6A97E637BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BA34B3BD-E616-4835-AEA8-6D6A97E637BC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5B3ECEB4-0E24-4040-9429-BA63333C6B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5B3ECEB4-0E24-4040-9429-BA63333C6B66}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5B3ECEB4-0E24-4040-9429-BA63333C6B66}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5B3ECEB4-0E24-4040-9429-BA63333C6B66}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1C052B5D-CA36-4AF0-89B3-C563F62D1B6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1C052B5D-CA36-4AF0-89B3-C563F62D1B6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1C052B5D-CA36-4AF0-89B3-C563F62D1B6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1C052B5D-CA36-4AF0-89B3-C563F62D1B6C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DB49720B-C0E5-42BB-A9C6-77D3DC065D09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DB49720B-C0E5-42BB-A9C6-77D3DC065D09}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DB49720B-C0E5-42BB-A9C6-77D3DC065D09}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DB49720B-C0E5-42BB-A9C6-77D3DC065D09}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BFF0A04C-5057-4A05-9122-CBF98C25DA33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFF0A04C-5057-4A05-9122-CBF98C25DA33}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFF0A04C-5057-4A05-9122-CBF98C25DA33}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFF0A04C-5057-4A05-9122-CBF98C25DA33}.Release|Any CPU.Build.0 = Release|Any CPU
+ {49091FD7-4936-42B3-9189-C18796B84269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49091FD7-4936-42B3-9189-C18796B84269}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {49091FD7-4936-42B3-9189-C18796B84269}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {49091FD7-4936-42B3-9189-C18796B84269}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5D749882-6633-415A-9DC3-5C7C208B29CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5D749882-6633-415A-9DC3-5C7C208B29CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5D749882-6633-415A-9DC3-5C7C208B29CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5D749882-6633-415A-9DC3-5C7C208B29CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BFC359F7-8FE7-4ABC-B192-8E9DE43460A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BFC359F7-8FE7-4ABC-B192-8E9DE43460A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BFC359F7-8FE7-4ABC-B192-8E9DE43460A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BFC359F7-8FE7-4ABC-B192-8E9DE43460A5}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9CC5413C-0C78-4D2C-9945-97D4F28C9A40}
+ EndGlobalSection
+EndGlobal
diff --git a/docs/event-hubs.md b/docs/event-hubs.md
new file mode 100644
index 0000000..a18522f
--- /dev/null
+++ b/docs/event-hubs.md
@@ -0,0 +1,268 @@
+
Azure Event Hubs
+
+
This library provides in-memory SDK for Azure Event Hubs which can be used as a drop-in replacement for the official
+Azure.Messaging.EventHubs SDK in your tests.
+
+> [!TIP]
+> See the whole [In-Memory Azure Test SDK](../README.md) suite if you are interested in other Azure services.
+
+## Recommended Usage
+
+To get started, add `Spotflow.InMemory.Azure.EventHubs` package to your project.
+
+```shell
+dotnet add Spotflow.InMemory.Azure.EventHubs
+```
+
+Create non-static factory class for creating the real Azure SDK clients. Relevant methods should be virtual to allow overriding as well as there should be a protected parameterless constructor for testing purposes.
+
+```cs
+class AzureClientFactory(TokenCredential tokenCredential)
+{
+ protected AzureClientFactory(): this(null!) {} // Testing-purposes only
+
+ public virtual EventHubProducerClient CreateProducerClient(string fullyQualifiedNamespace, string eventHubName)
+ {
+ return new EventHubProducerClient(fullyQualifiedNamespace, eventHubName, tokenCredential);
+ }
+}
+```
+
+Use this class to obtain EventHub clients in the tested code:
+
+```cs
+class ExampleService(AzureClientFactory clientFactory, string fullyQualifiedNamespace, string eventHubName)
+{
+ private readonly EventHubProducerClient _client = clientFactory.CreateProducerClient(fullyQualifiedNamespace, eventHubName);
+
+ public async Task SendEventAsync(BinaryData payload)
+ {
+ await _client.SendAsync(new EventData(payload));
+ }
+}
+```
+
+Create `InMemoryAzureClientFactory` by inheriting `AzureClientFactory` and override relevant factory methods to return in-memory clients:
+
+```cs
+class InMemoryAzureClientFactory(InMemoryEventHubProvider provider): AzureClientFactory
+{
+ public override EventHubProducerClient CreateProducerClient(string fullyQualifiedNamespace, string eventHubName)
+ {
+ return new InMemoryEventHubProducerClient(fullyQualifiedNamespace, eventHubName, NoOpTokenCredential.Instance, provider);
+ }
+}
+```
+
+When testing, it is now enough to initialize `InMemoryEventHubProvider` and inject `InMemoryAzureClientFactory` to the tested code (e.g. via Dependency Injection):
+
+```cs
+var provider = new InMemoryEventHubProvider();
+var eventHub = provider.AddNamespace().AddEventHub("test-event-hub", numberOfPartitions: 4);
+
+var services = new ServiceCollection();
+
+services.AddSingleton();
+services.AddSingleton(provider);
+services.AddSingleton();
+
+var exampleService = services.BuildServiceProvider().GetRequiredService();
+
+var payload = BinaryData.FromString("test-data");
+
+await exampleService.SendEventAsync(eventHub.Namespace.Hostname, eventHub.Name, payload);
+
+var receiver = InMemoryPartitionReceiver.FromEventHub(partitionId: "0", eventHub);
+
+var batch = await receiver.ReceiveBatchAsync(100, TimeSpan.Zero);
+
+batch.Should().ContainSingle(e => e.EventBody.ToString() == "test-data");
+```
+
+## Fault Injection
+
+Fault injections let you simulate transient and persistent faults in Azure Event Hub.
+Thanks to that you can test how your application behaves in case of Azure outages, network issues, timeouts, etc.
+
+To inject a fault, you need to use the [concept of hooks](./hooks.md) - functions that are called before or after the actual operation is executed.
+A new hook can be registered by calling the `AddHook` method on the `InMemoryEventHubProvider` instance.
+You can build fault hook by calling the `Faults` method on the hook context and then calling the appropriate method, e.g. `ServiceIsBusy`:
+
+For overview of available hooks, please see the [Hooks](#hooks) section.
+
+```cs
+var provider = new InMemoryEventHubProvider();
+var hook = provider.AddHook(hookBuilder => hookBuilder.Before(ctx => ctx.Faults().ServiceIsBusy()));
+```
+
+The `AddHook` method gives you a builder that lets you define which operations the hook should apply to.
+In the example above, the hook affects all Event Hub operations.
+However, you can limit it to specific operations, like `Send`, or target specific scopes, such as operation on Event Hub called `my-eventhub`:
+
+```cs
+var hook = provider.AddHook(hookBuilder => hookBuilder.ForProducer(eventHubName: "my-event-hub")
+ .BeforeSend(ctx => ctx.Faults().ServiceIsBusy()));
+```
+
+You can control when the hook should execute via the `IHookRegistration` interface returned by the `AddHook` method.
+By default, the hook is enabled, but you can disable it by calling the `Disable` method.
+To simulate temporary outages, use the `DisableAfter` method to limit the number of fault occurrences.
+
+See a full example of fault injection below:
+
+```cs
+var provider = new InMemoryEventHubProvider();
+
+var hook = provider.AddHook(hook => hook.Before(ctx => ctx.Faults().ServiceIsBusy()));
+
+var eventHub = provider.AddNamespace("test-ns").AddEventHub("test-eh", 1);
+
+var producerClient = InMemoryEventHubProducerClient.FromEventHub(eventHub);
+
+var act = () => producerClient.SendAsync([new EventData()]);
+
+await act.Should().ThrowAsync().WithMessage("Event hub 'test-eh' in namespace 'test-ns' is busy. (test-eh). *");
+
+hook.Disable();
+
+await act.Should().NotThrowAsync();
+```
+
+## Delay Simulation
+
+Delay simulation is currently not supported for Azure Event Hub.
+
+However, [hooks](hooks.md) can be used to simulate custom delays. For overview of available hooks, please see the [Hooks](#hooks) section.
+
+## Supported APIs and features
+
+### SDK clients & methods
+
+Following SDK clients and their method groups and properties are supported.
+
+Async versions of these methods are also supported. All supported async methods starts with [Task.Yield()](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield) to force the method to complete asynchronously.
+
+Other methods and properties are not supported and will throw `NotSupportedException`.
+
+Clients are thread-safe.
+
+#### `InMemoryEventHubProducerClient : EventHubProducerClient`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `EventHubName` | |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `IsClosed` | |
+
+| Method group |
+| ----------------------------- |
+| `CloseAsync` |
+| `CreateBatchAsync` |
+| `DisposeAsync` |
+| `CreateBatchAsync` |
+| `GetEventHubPropertiesAsync` |
+| `GetPartitionIdsAsync` |
+| `GetPartitionPropertiesAsync` |
+
+| Constructors & factory methods | Note |
+| ---------------------------------------------------------------------------------------- | ---- |
+| `(string connectionString)` | |
+| `(string fullyQualifiedNamespace, string eventHubName, TokenCredential tokenCredential)` | |
+| `(EventHubConnection connection)` | |
+| `FromEventHub(InMemoryEventHub eventHub)` | |
+| `FromNamespace(InMemoryEventHubNamespace eventHubNamespace, string eventHubName)` | |
+
+#### `InMemoryEventHubConsumerClient : EventHubConsumerClient`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `ConsumerGroup` | |
+| `EventHubName` | |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `IsClosed` | |
+
+| Method group |
+| ----------------------------- |
+| `CloseAsync` |
+| `DisposeAsync` |
+| `GetEventHubPropertiesAsync` |
+| `GetPartitionIdsAsync` |
+| `GetPartitionPropertiesAsync` |
+
+| Constructors & factory methods | Note |
+| -------------------------------------------------------------------------------------------------------------- | ---- |
+| `(string consumerGroup, string connectionString)` | |
+| `(string consumerGroup, string fullyQualifiedNamespace, string eventHubName, TokenCredential tokenCredential)` | |
+| `(string consumerGroup, EventHubConnection connection)` | |
+| `FromEventHub(string consumerGroup, InMemoryEventHub eventHub)` | |
+| `FromNamespace(string consumerGroup, InMemoryEventHubNamespace eventHubNamespace, string eventHubName)` | |
+
+#### `InMemoryPartitionReceiver : PartitionReceiver`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `ConsumerGroup` | |
+| `EventHubName` | |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `InitialPosition` | |
+| `IsClosed` | |
+| `PartitionId` | |
+
+| Method group |
+| --------------------------------- |
+| `GetPartitionPropertiesAsync` |
+| `ReadLastEnqueuedEventProperties` |
+| `ReceiveBatchAsync` |
+| `DisposeAsync` |
+| `CloseAsync` |
+
+| Constructors & factory methods | Note |
+| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
+| `(string consumerGroup, string partitionId, EventPosition eventPosition, string connectionString)` | No credentials are validated. |
+| `(string consumerGroup, string partitionId, EventPosition eventPosition, string connectionString, string eventHubName)` | No credentials are validated. |
+| `(string consumerGroup, string partitionId, EventPosition eventPosition, string fullyQualifiedNamespace, string eventHubName, TokenCredential credential)` | No credentials are validated. |
+| `(string consumerGroup, string partitionId, EventPosition eventPosition, EventHubConnection connection)` | No credentials are validated. |
+| `FromNamespace(string consumerGroup, string partitionId, EventPosition startingPosition, InMemoryEventHubNamespace eventHubNamespace, string eventHubName)` | No credentials are validated. |
+| `FromEventHub(string consumerGroup, string partitionId, EventPosition startingPosition, InMemoryEventHub eventHub)` | No credentials are validated. |
+| `FromEventHub(string partitionId, EventPosition startingPosition, InMemoryEventHub eventHub)` | Default consumer group is used. No credentials are validated. |
+
+### Features
+
+For the supported methods enumerated above, not all features are fully implemented.
+
+If the given feature is not supported, than the method will just ignore any parameters related to that feature.
+
+| Feature | Is Supported |
+| ---------------------------------------------------------------------------- | ------------ |
+| Batches | ✅ |
+| Event System Property - Content Type | ✅ |
+| Event System Property - Correlation Id | ✅ |
+| Event System Property - Message Id | ✅ |
+| Offset-based starting positions | ❌ |
+| Partition keys | ✅ |
+| Properties - Event Hub | ✅ |
+| Properties - Partition | ✅ |
+| Randomization of initial sequence numbers for event hub partitions | ✅ |
+| Sequence number based starting positions (including `Earliest` and `Latest`) | ✅ |
+
+## Hooks
+
+Following hooks are supported in both `Before` and `After` variants:
+
+- All `Event Hub` operations
+ - All `Producer` operations
+ - `Send`
+ - All `Consumer` operations
+ - `ReceiveBatch`
+
+For details about concept of hooks, please see the [Hooks](./hooks.md) page.
diff --git a/docs/hooks.md b/docs/hooks.md
new file mode 100644
index 0000000..c4a0b46
--- /dev/null
+++ b/docs/hooks.md
@@ -0,0 +1,55 @@
+# Hooks
+
+Hooks are a way to extend the functionality of the core library. Hooks enable to inject some custom behavior before and after client operations.
+
+User can register before and after hooks by calling `AddHook` method on resource providers such as [`InMemoryStorageProvider`](./storage.md). When adding a hook, user targets one or more client operations to which the hook should be applied and a function that should be executed before or after the operation. This function receives a context which contains information about the currently executed operation.
+
+```csharp
+var provider = new InMemoryStorageProvider();
+
+// Register hook
+
+// Before all storage operations:
+
+provider.AddHook(hook => hook.Before(context => { ... }));
+
+// Before creating any container in any storage account:
+
+provider.AddHook(hook => hook.ForBlobService().ForContainerOperations().BeforeCreate(context => { ... }));
+
+// Before creating specific container in any storage account:
+
+provider.AddHook(hook => hook
+ .ForBlobService()
+ .ForContainerOperations(containerName: "container")
+ .BeforeCreate(context => { ... }));
+
+// After creating any container or uploading any blob within a specific storage account:
+
+provider.AddHook(hook => hook
+ .ForBlobService(storageAccountName: "account")
+ .ForContainerOperations()
+ .After(context => { ... }), containerOperations: ContainerOperations.Create, blobOperations: BlobOperations.Upload);
+```
+
+## Contexts
+
+The contexts passed to the hook functions are organized into an inheritance hierarchy. For example, after hook for blob upload receives `BlobUploadAfterHookContext` type which inherits from the `BlobAfterHookContext` -> `BlobServiceAfterHookContext` -> `StorageAfterHookContext` hierarchy.
+
+This enabled the hook functions to access specific information about the currently executed operation even if the hook targets a more general group of operations (e.g. all storage operations). This can be done e.g via pattern matching:
+
+```csharp
+provider.AddHook(hook => hook.Before(hookFunc));
+
+static Task hookFunc(StorageAfterHookContext context)
+{
+ if(context is BlobUploadAfterHookContext blobUpload)
+ {
+ ...
+ }
+ else
+ {
+ ...
+ }
+}
+```
diff --git a/docs/images/intro.excalidraw.svg b/docs/images/intro.excalidraw.svg
new file mode 100644
index 0000000..e72bbd5
--- /dev/null
+++ b/docs/images/intro.excalidraw.svg
@@ -0,0 +1,21 @@
+
\ No newline at end of file
diff --git a/docs/key-vault.md b/docs/key-vault.md
new file mode 100644
index 0000000..864718b
--- /dev/null
+++ b/docs/key-vault.md
@@ -0,0 +1,153 @@
+
Azure Key Vault
+
+
This library provides in-memory SDK for Azure Key Vault which can be used as a drop-in replacement for the official
+`Azure.Security.KeyVault.*` SDKs in your tests.
+
+> [!TIP]
+> See the whole [In-Memory Azure Test SDK](../README.md) suite if you are interested in other Azure services.
+
+## Recommended Usage
+
+To get started, add `Spotflow.InMemory.Azure.KeyVault` package to your project.
+
+```shell
+dotnet add Spotflow.InMemory.Azure.KeyVault
+```
+
+Create non-static factory class for creating the real Azure SDK clients. Relevant methods should be virtual to allow overriding as well as there should be a protected parameterless constructor for testing purposes.
+
+```cs
+class AzureClientFactory(TokenCredential tokenCredential)
+{
+ protected AzureClientFactory(): this(null!) {} // Testing-purposes only
+
+ public virtual SecretClient CreateSecretClient(Uri vaultUri) => new(vaultUri, tokenCredential);
+}
+```
+
+Use this class to obtain Key Vault clients in the tested code:
+
+```cs
+class ExampleService(AzureClientFactory clientFactory, Uri vaultUri)
+{
+ private readonly SecretClient _client = clientFactory.CreateSecretClient(vaultUri);
+
+ public async Task GetSecretAsync(string secretName)
+ {
+ var response = await _client.GetSecretAsync(secretName);
+ return response.Value.Value;
+ }
+}
+```
+
+Create `InMemoryAzureClientFactory` by inheriting `AzureClientFactory` and override relevant factory methods to return in-memory clients:
+
+```cs
+class InMemoryAzureClientFactory(InMemoryEventHubProvider provider): AzureClientFactory
+{
+ public override SecretClient CreateSecretClient(Uri vaultUri)
+ {
+ return new InMemorySecretClient(vaultUri, provider);
+ }
+}
+```
+
+When testing, it is now enough to initialize `InMemoryKeyVaultProvider` and inject `InMemoryAzureClientFactory` to the tested code (e.g. via Dependency Injection):
+
+```cs
+var provider = new InMemoryKeyVaultProvider();
+var vault = provider.AddVault();
+
+var services = new ServiceCollection();
+
+services.AddSingleton();
+services.AddSingleton(provider);
+services.AddSingleton();
+
+var exampleService = services.BuildServiceProvider().GetRequiredService();
+
+var secret = exampleService.GetSecretAsync("my-secret");
+```
+
+## Fault Injection
+
+Fault injection is currently not supported for Azure Key Vault.
+
+However, [hooks](hooks.md) can be used to simulate custom faults. For overview of available hooks, please see the [Hooks](#hooks) section.
+
+## Delay Simulation
+
+Delay simulation is currently not supported for Azure Key Vault.
+
+However, [hooks](hooks.md) can be used to simulate custom delays. For overview of available hooks, please see the [Hooks](#hooks) section.
+
+## Supported APIs and features
+
+### SDK clients & methods
+
+Following SDK clients and their method groups and properties are supported.
+
+Async versions of these methods are also supported. All supported async methods are guaranteed executed truly asynchronously by using [Task.Yield()](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield) or [ConfigureAwaitOptions.ForceYielding](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.configureawaitoptions).
+
+Other methods and properties are not supported and will throw `NotSupportedException`.
+
+Clients are thread-safe.
+
+#### `InMemorySecretClient : SecretClient`
+
+| Property | Note |
+| ---------- | ---- |
+| `VaultUri` | |
+
+| Method group |
+| ------------------------------- |
+| `GetSecret` |
+| `SetSecret` |
+| `GetPropertiesOfSecrets` |
+| `GetPropertiesOfSecretVersions` |
+| `StartDeleteSecret` |
+| `UpdateSecretProperties` |
+
+| Constructors & factory methods | Note |
+| ----------------------------------- | ---- |
+| `(Uri vaultUri)` | |
+| `FromVault(InMemoryKeyVault vault)` | |
+
+### Features
+
+For the supported methods enumerated above, not all features are fully implemented.
+
+If the given feature is not supported, than the method will just ignore any parameters related to that feature.
+
+| Feature | Is Supported |
+| ---------------------------------------- | ------------ |
+| Secrets - Properties - `ContentType` | ✅ |
+| Secrets - Properties - `CreatedOn` | ✅ |
+| Secrets - Properties - `Enabled` | ✅ |
+| Secrets - Properties - `ExpiresOn` | ✅ |
+| Secrets - Properties - `NotBefore` | ✅ |
+| Secrets - Properties - `RecoverableDays` | ❌ |
+| Secrets - Properties - `RecoveryLevel` | ❌ |
+| Secrets - Properties - `Tags` | ✅ |
+| Secrets - Properties - `UpdatedOn` | ✅ |
+| Secrets - Purge | ❌ |
+| Secrets - Recovery | ❌ |
+| Secrets - Soft-delete | ✅ |
+| Secrets - Versioning | ✅ |
+
+## Hooks
+
+Following hooks are supported in both `Before` and `After` variants:
+
+- All `Key Vault` operations
+ - All `Secret` operations
+ - `GetSecret`
+ - `SetSecret`
+
+For details about concept of hooks, please see the [Hooks](./hooks.md) page.
diff --git a/docs/service-bus.md b/docs/service-bus.md
new file mode 100644
index 0000000..e581a15
--- /dev/null
+++ b/docs/service-bus.md
@@ -0,0 +1,271 @@
+
Azure Service Bus
+
+
This library provides in-memory SDK for Azure Event Hubs which can be used as a drop-in replacement for the official
+Azure.Messaging.ServiceBus in your tests.
+
+> [!TIP]
+> See the whole [In-Memory Azure Test SDK](../README.md) suite if you are interested in other Azure services.
+
+## Recommended Usage
+
+To get started, add `Spotflow.InMemory.Azure.EventHubs` package to your project.
+
+```shell
+dotnet add Spotflow.InMemory.Azure.ServiceBus
+```
+
+Create non-static factory class for creating the real Azure SDK clients. Relevant methods should be virtual to allow overriding as well as there should be a protected parameterless constructor for testing purposes.
+
+```cs
+class AzureClientFactory(TokenCredential tokenCredential)
+{
+ protected AzureClientFactory(): this(null!) {} // Testing-purposes only
+
+ public virtual ServiceBusSender CreateServiceBusSender(string fullyQualifiedNamespace, string queueOrTopicName)
+ {
+ return new ServiceBusClient(fullyQualifiedNamespace, tokenCredential).CreateSender(queueOrTopicName);
+ }
+}
+```
+
+Use this class to obtain ServiceBus clients in the tested code:
+
+```cs
+class ExampleService(AzureClientFactory clientFactory, string fullyQualifiedNamespace, string queueName)
+{
+ private readonly ServiceBusSender _client = clientFactory.CreateServiceBusSender(fullyQualifiedNamespace, queueName);
+
+ public async Task SendMessageAsync(BinaryData payload)
+ {
+ await _client.SendMessageAsync(new ServiceBusMessage(payload));
+ }
+}
+```
+
+Create `InMemoryAzureClientFactory` by inheriting `AzureClientFactory` and override relevant factory methods to return in-memory clients:
+
+```cs
+class InMemoryAzureClientFactory(InMemoryServiceBusProvider provider) : AzureClientFactory
+{
+ public override ServiceBusSender CreateServiceBusSender(string fullyQualifiedNamespace, string queueOrTopicName)
+ {
+ return new InMemoryServiceBusClient(fullyQualifiedNamespace, NoOpTokenCredential.Instance, provider).CreateSender(queueOrTopicName);
+ }
+}
+```
+
+When testing, it is now enough to initialize `InMemoryServiceBusProvider` and inject `InMemoryAzureClientFactory` to the tested code (e.g. via Dependency Injection):
+
+```csharp
+var provider = new InMemoryServiceBusProvider();
+var queue = serviceBusProvider.AddNamespace().AddQueue("my-queue");
+
+var services = new ServiceCollection();
+
+services.AddSingleton();
+services.AddSingleton(provider);
+services.AddSingleton();
+
+var exampleService = services.BuildServiceProvider().GetRequiredService();
+
+var payload = BinaryData.FromString("test-data");
+
+await exampleService.SendMessageAsync(queue.Namespace.FullyQualifiedNamespace, queue.QueueName, payload);
+
+await using var client = InMemoryServiceBusClient.FromNamespace(queue.Namespace, provider);
+
+await using var receiver = client.CreateReceiver(queue.QueueName);
+
+var message = await receiver.ReceiveMessageAsync();
+
+message.Body.ToString().Should().Be("test-data");
+
+await receiver.CompleteMessageAsync(message);
+```
+
+## Fault Injection
+
+Fault injection is currently not supported for Azure Service Bus.
+
+However, [hooks](hooks.md) can be used to simulate custom faults. For overview of available hooks, please see the [Hooks](#hooks) section.
+
+## Delay Simulation
+
+Delay simulation is currently not supported for Azure Service Bus.
+
+However, [hooks](hooks.md) can be used to simulate custom delays. For overview of available hooks, please see the [Hooks](#hooks) section.
+
+## Supported APIs and features
+
+### SDK clients & methods
+
+Following SDK clients and their method groups and properties are supported.
+
+Async versions of these methods are also supported. All supported async methods starts with [Task.Yield()](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield) to force the method to complete asynchronously.
+
+Other methods and properties are not supported and will throw `NotSupportedException`.
+
+Clients are thread-safe.
+
+#### `InMemoryServiceBusClient: ServiceBusClient`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `IsClosed` | |
+| `TransportType` | |
+
+| Method group |
+| ------------------------ |
+| `AcceptNextSessionAsync` |
+| `AcceptSessionAsync` |
+| `CreateSender` |
+| `CreateReceiver` |
+| `DisposeAsync` |
+
+| Constructors & factory methods | Note |
+| --------------------------------------------------------------------------------------------------------- | ----------------------------- |
+| `(string connectionString)` | No credentials are validated. |
+| `(string connectionString, ServiceBusClientOptions options)` | No credentials are validated. |
+| `(string fullyQualifiedNamespace, TokenCredential credential)` | No credentials are validated. |
+| `(string fullyQualifiedNamespace, TokenCredential credential, ServiceBusClientOptions options)` | No credentials are validated. |
+| `FromNamespace(InMemoryServiceBusNamespace serviceBusNamespace, ServiceBusClientOptions? options = null)` | |
+
+#### `InMemoryServiceBusSender : ServiceBusSender`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `EntityPath` | |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `IsClosed` | |
+
+| Method group |
+| ------------------------- |
+| `SendMessageAsync` |
+| `SendMessagesAsync` |
+| `CreateMessageBatchAsync` |
+| `DisposeAsync` |
+| `CloseAsync` |
+
+| Constructors & factory methods | Note |
+| --------------------------------------------------------------------------------------------- | ----------------------------- |
+| `(InMemoryServiceBusClient client, string queueOrTopicName)` | No credentials are validated. |
+| `(InMemoryServiceBusClient client, string queueOrTopicName, ServiceBusSenderOptions options)` | No credentials are validated. |
+| `FromQueue(InMemoryServiceBusQueue queue, ServiceBusClientOptions? options = null)` | |
+| `FromTopic(InMemoryServiceBusTopic topic, ServiceBusClientOptions? options = null)` | |
+
+#### `InMemoryServiceBusReceiver: ServiceBusReceiver`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `EntityPath` | |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `IsClosed` | |
+| `PrefetchCount` | |
+| `ReceiveMode` | |
+
+| Method group |
+| ----------------------- |
+| `AbandonMessageAsync` |
+| `CloseAsync` |
+| `CompleteMessageAsync` |
+| `DisposeAsync` |
+| `ReceiveMessageAsync` |
+| `ReceiveMessagesAsync` |
+| `RenewMessageLockAsync` |
+
+| Constructors & factory methods | Note |
+| --------------------------------------------------------------------------------------------------------------- | ----------------------------- |
+| `(InMemoryServiceBusClient client, string queueName)` | No credentials are validated. |
+| `(InMemoryServiceBusClient client, string queueName, ServiceBusSenderOptions options)` | No credentials are validated. |
+| `(InMemoryServiceBusClient client, string queueName, string subscriptionName)` | No credentials are validated. |
+| `(InMemoryServiceBusClient client, string queueName, string subscriptionName, ServiceBusSenderOptions options)` | No credentials are validated. |
+| `FromQueue(InMemoryServiceBusQueue queue, ServiceBusClientOptions? options = null)` | |
+| `FromSubscription(InMemoryServiceBusTopicSubscription subscription, ServiceBusClientOptions? options = null)` | |
+
+#### `InMemoryServiceBusSessionReceiver : ServiceBusSessionReceiver`
+
+| Property | Note |
+| ------------------------- | ---- |
+| `EntityPath` | |
+| `FullyQualifiedNamespace` | |
+| `Identifier` | |
+| `IsClosed` | |
+| `PrefetchCount` | |
+| `ReceiveMode` | |
+| `SessionId` | |
+| `SessionLockedUntil` | |
+
+| Method group |
+| ----------------------- |
+| `AbandonMessageAsync` |
+| `CloseAsync` |
+| `CompleteMessageAsync` |
+| `DisposeAsync` |
+| `GetSessionStateAsync` |
+| `ReceiveMessageAsync` |
+| `ReceiveMessagesAsync` |
+| `RenewMessageLockAsync` |
+| `RenewSessionLockAsync` |
+| `SetSessionStateAsync` |
+
+No public constructors are available.
+
+### Features
+
+For the supported methods enumerated above, not all features are fully implemented.
+
+If the given feature is not supported, than the method will just ignore any parameters related to that feature.
+
+| Feature | Is Supported |
+| ------------------------------- | ------------ |
+| Deferred messages | ❌ |
+| Dead-letter queues | ❌ |
+| `PeekLock` receive mode | ✅ |
+| Processors | ❌ |
+| Queues | ✅ |
+| `ReceiveAndDelete` receive mode | ✅ |
+| Rules | ❌ |
+| Scheduled messages | ❌ |
+| Sessions | ✅ |
+| Session states | ✅ |
+| Sequence numbers | ✅ |
+| Subscriptions | ✅ |
+| Topics | ✅ |
+
+## Available Fluent Assertions
+
+There are following assertions available for in-memory service bus types:
+
+### `InMemoryServiceBusQueue`
+
+- `.Should().BeEmptyAsync()`
+
+### `InMemoryServiceBusTopicSubscription`
+
+- `.Should().BeEmptyAsync()`
+
+## Hooks
+
+Following hooks are supported in both `Before` and `After` variants:
+
+- All `Service Bus` operations
+ - All `Producer` operations
+ - `SendMessage`
+ - `SendBatch`
+ - All `Consumer` operations
+ - `ReceiveMessage`
+ - `ReceiveBatch`
+
+For details about concept of hooks, please see the [Hooks](./hooks.md) page.
diff --git a/docs/storage.md b/docs/storage.md
new file mode 100644
index 0000000..25e8128
--- /dev/null
+++ b/docs/storage.md
@@ -0,0 +1,465 @@
+
Azure Storage
+
+
This library provides in-memory SDK for Azure Storage which can be used as a drop-in replacement for the official
+Azure.Storage.Blobs and
+Azure.Data.Tables SDKs in your tests.