<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Beniamin Lenarcik]]></title><description><![CDATA[Beniamin Lenarcik]]></description><link>https://beniaminlenarcik.pl</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 01:09:09 GMT</lastBuildDate><atom:link href="https://beniaminlenarcik.pl/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Efficiently Manage AI Agent Context in .NET]]></title><description><![CDATA[Introduction
Modern platforms for building AI agents significantly accelerate the development of such solutions. They allow you to easily select a large language model (LLM), manage prompts, and defin]]></description><link>https://beniaminlenarcik.pl/how-to-efficiently-manage-ai-agent-context-in-net</link><guid isPermaLink="true">https://beniaminlenarcik.pl/how-to-efficiently-manage-ai-agent-context-in-net</guid><category><![CDATA[dotnet]]></category><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[agentic AI]]></category><category><![CDATA[semantic kernel]]></category><category><![CDATA[observation-masking]]></category><category><![CDATA[LM-summarization]]></category><category><![CDATA[context engineering]]></category><category><![CDATA[Context Management]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Wed, 08 Apr 2026 20:49:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/670fba783947e083caa6f5b9/345d92d3-77c9-469c-a089-bcd3dd8f5567.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>Modern platforms for building AI agents significantly accelerate the development of such solutions. They allow you to easily select a large language model (LLM), manage prompts, and define tools that the agent can use automatically.</p>
<p>However, the real challenge on the developer’s side is ensuring the model receives the right context. Frameworks such as <a href="https://docs.langchain.com/oss/python/langchain/messages">LangChain</a> in Python or <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/chat-history?pivots=programming-language-csharp">Semantic Kernel</a> in .NET expect that with every request we provide conversation history or other data describing the current interaction state.</p>
<p>Intuitively, passing the entire conversation history every time seems reasonable. It should help the agent better understand context and make more accurate decisions. In practice, however, several questions arise:</p>
<ul>
<li><p>Does increasing the number of tokens in the prompt always improve responses?</p>
</li>
<li><p>How do we avoid exceeding the context length limits of language models?</p>
</li>
<li><p>What about the growing processing time and cost of subsequent agent calls?</p>
</li>
</ul>
<p>In this article, I’ll show how to manage context passed to large language models using a .NET agent built with Semantic Kernel. I’ll present approaches that help control context size, ensuring stable costs and consistent solution quality.</p>
<h1>Context Window – Limitation of LLMs</h1>
<p>Large Language Models have a limit on the maximum context size, known as the <em>context window</em>. It includes all tokens used in a single model call — both input and output. Depending on the model, this limit can be, for example, 128k tokens (<a href="https://developers.openai.com/api/docs/models/gpt-5-chat-latest"><strong>OpenAI GPT-5</strong></a>).</p>
<p>In practice, when building AI agents, this limit must include:</p>
<ul>
<li><p>system prompt</p>
</li>
<li><p>conversation history</p>
</li>
<li><p>tool outputs</p>
</li>
<li><p>new context (e.g., user query or system event)</p>
</li>
</ul>
<p>Since the context window also includes output tokens, too much input data reduces space available for reasoning and response generation.</p>
<h1>Does More Context Always Help?</h1>
<p>Intuitively, more context should improve results — but this is not always true. The paper <a href="https://arxiv.org/abs/2307.03172"><em><strong>Lost in the Middle: How Language Models Use Long Contexts</strong></em></a> shows that increasing the number of documents in a prompt eventually stops improving answer quality. Additionally, models tend to use information at the beginning and end of the context most effectively, while performing worst on information in the middle (the “lost in the middle” effect).</p>
<h1>How to Manage AI Agent Context</h1>
<p>If more context does not always improve results, the natural question is: how should we manage it?</p>
<p>This topic is explained in more detail in the article <a href="https://blog.jetbrains.com/research/2025/12/efficient-context-management/"><strong>Cutting Through the Noise: Smarter Context Management for LLM-Powered Agents</strong></a>, which focuses on effective context management in systems based on LLMs.</p>
<p>In practice, there are two main approaches:</p>
<ul>
<li><p><strong>LLM summarization</strong> – reducing the conversation history to a shorter version,</p>
</li>
<li><p><strong>Observation masking</strong> – limiting how much data is passed to the model.</p>
</li>
</ul>
<p>Each approach has its trade-offs. Summarization controls the context size better, but it adds extra cost and may lose some details. Masking is simpler and cheaper, but it does not stop the context from growing over time.</p>
<p>In practice, the best results come from a hybrid approach that combines both methods. In the next sections, I will show how to implement them using Semantic Kernel in .NET.</p>
<h1>Context Management in Semantic Kernel (.NET)</h1>
<h2>AI Product Recommendation Agent</h2>
<p>Imagine an online computer store where an AI agent helps users choose a laptop based on preferences such as: budget, weight, battery life and use case. The agent gathers requirements step by step, searches products, analyzes descriptions, and provides recommendations.</p>
<p>First, we create a basic agent, and then we implement context management strategies — observation masking and LLM summarization. We will analyze their impact on the number of input tokens and total tokens using additional analytics built in the project.</p>
<p>The source code used in this article is available in the GitHub repository <a href="https://github.com/L3mur1/SemanticKernelContextManagement"><strong>SemanticKernelContextManagement</strong></a>.</p>
<h2>Basic AI Agent</h2>
<p>To build the agent in .NET, we will use the Semantic Kernel framework. A simple “Hello World” example can be easily created based on the <a href="https://learn.microsoft.com/en-us/semantic-kernel/get-started/quick-start-guide?pivots=programming-language-csharp">official documentation</a>.</p>
<p>For this experiment, we will add a <a href="https://github.com/L3mur1/SemanticKernelContextManagement/blob/master/SemanticKernelContextManagement/Products/SemanticsKernel/ProductsPlugin.cs">ProductsPlugin</a> that returns information about laptops. To keep things simple, the data will be stored in static JSON files inside the project and loaded into memory.</p>
<p>For recommendations:</p>
<ul>
<li>A general one, which returns short information about all laptops:</li>
</ul>
<pre><code class="language-csharp">[KernelFunction]
        [Description("Get all products from shop. Returns only product names and short summaries.")]
        public string GetProducts()
        {
            var productSummaries = products.Select(p =&gt; new
            {
                p.Name,
                p.ShortSummary,
            });

            return JsonSerializer.Serialize(productSummaries, ProductJsonOptions);
        }
</code></pre>
<p>A detailed one, for a single product (laptop):</p>
<pre><code class="language-csharp">        [Description("Get full detailed description of a product by its name.")]
        public string GetProductDetails([Description("Product name, e.g. Laptop Pro 14")] string productName)
        {
            var product = products.FirstOrDefault(p =&gt; p.Name.Equals(productName, StringComparison.OrdinalIgnoreCase));
            if (product == null)
            {
                return $"Product not found: {productName}";
            }

            return JsonSerializer.Serialize(product, ProductJsonOptions);
        }
</code></pre>
<p>In the first request, we also include a system prompt that tells the model to act as a shop assistant. One useful advantage of such an agent is that the assistant will always reply in the user’s language 🙂:</p>
<pre><code class="language-csharp">private const string SystemPrompt = """
            You are a shop assistant that recommends products from our catalog.
            Use the Products plugin (GetProducts, GetProductDetails) to read real catalog data before suggesting items.
            Do not invent products, prices, or stock. If nothing matches, say so clearly.
            Match the user's language in your replies.
            """;
</code></pre>
<p>In Semantic Kernel, we can decide whether the agent should use the tools defined in the plugin. In this case, we use <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/function-calling/function-choice-behaviors?pivots=programming-language-csharp">FunctionChoiceBehavior.Auto</a>, which means the agent can use the functions, but it is not required to:</p>
<pre><code class="language-csharp">        private readonly OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
        };
</code></pre>
<p>We build the program as a console application. We take the user’s request from the console, for example: “I want to buy a laptop.” In the basic approach, we add the question to the context as a <code>UserMessage</code> and send everything to the LLM. After receiving the response, we also save it in the history and return it to the user:</p>
<pre><code class="language-csharp">    public async Task&lt;string&gt; GetRecommendationAsync(string userInput)
    {
        ChatHistory.AddUserMessage(userInput);

        var result = await chatCompletionService.GetChatMessageContentAsync(
            ChatHistory,
            executionSettings: openAIPromptExecutionSettings,
            kernel: kernel);

        if (string.IsNullOrWhiteSpace(result.Content))
        {
            throw new InvalidOperationException($"{nameof(chatCompletionService)} did not return any content.");
        }

        ChatHistory.Add(result);
        return result.Content;
    }
</code></pre>
<p>Example conversation with the assistant in the console:</p>
<img src="https://cdn.hashnode.com/uploads/covers/670fba783947e083caa6f5b9/7d35afa8-ecd9-4b77-8b75-b16041f39ea9.png" alt="" style="display:block;margin:0 auto" />

<h2>Observation masking</h2>
<p>In agents built on the Semantic Kernel platform, raw tool results are added one below another to the conversation history as messages with the role <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.semantickernel.chatcompletion.authorrole.tool?view=semantic-kernel-dotnet#microsoft-semantickernel-chatcompletion-authorrole-tool">AuthorRole.Tool</a>. They are not visible to the client of our store. We return to the user a response processed by the agent, generated based on those results.</p>
<p>In the case of the function <code>GetProductDetails</code>, the result is a JSON containing full information about a specific laptop. Let’s assume that in most use cases, the same product information will be included in the response generated by the assistant. We make a deliberate decision to introduce observation masking. In this way, we reduce costs and response time in exchange for lower quality — a higher risk of hallucinations and a possible increase in the number of repeated tool calls.</p>
<p>We mask tool results by introducing a placeholder in their place:</p>
<pre><code class="language-csharp">private const string ObservationMaskedPlaceholder = "[TOOL_OBSERVATION_MASKED]";
</code></pre>
<p>We introduce the mechanism just before returning the response to the client, so that it takes effect for the next user query:</p>
<pre><code class="language-csharp">    public async Task&lt;string&gt; GetRecommendationAsync(string userInput)
    {
        ChatHistory.AddUserMessage(userInput);

        var result = await chatCompletionService.GetChatMessageContentAsync(
            ChatHistory,
            executionSettings: openAIPromptExecutionSettings,
            kernel: kernel);

        if (string.IsNullOrWhiteSpace(result.Content))
        {
            throw new InvalidOperationException($"{nameof(chatCompletionService)} did not return any content.");
        }

        ChatHistory.Add(result);

        // maskowanie obserwacji
        if (useObservationMasking)
        {
            MaskObservations();
        }

        return result.Content;
    }
</code></pre>
<pre><code class="language-csharp">        private void MaskObservations()
        {
            var toolMessages = ChatHistory.Where(m =&gt; m.Role == AuthorRole.Tool);
            foreach (var message in toolMessages)
            {
                message.Content = ObservationMaskedPlaceholder;

                if (message.Items.Count == 0)
                {
                    message.Items.Add(new TextContent(ObservationMaskedPlaceholder));
                    continue;
                }

                var originals = message.Items.ToArray();
                message.Items.Clear();

                foreach (var item in originals)
                {
                    if (item is FunctionResultContent functionResult)
                    {
                        message.Items.Add(new FunctionResultContent(
                            functionResult.FunctionName,
                            functionResult.PluginName,
                            functionResult.CallId,
                            ObservationMaskedPlaceholder));
                    }
                    else
                    {
                        message.Items.Add(new TextContent(ObservationMaskedPlaceholder));
                    }
                }
            }
        }
</code></pre>
<h2>LLM summarization</h2>
<p>Let’s imagine that customers of our store ask many questions about laptops during a single conversation. As the interaction history grows, the number of tokens sent to the model increases. As a result, subsequent responses are generated more and more slowly, the processing cost grows linearly, and the quality of the responses does not significantly improve. In this situation, we can deliberately introduce periodic AI-based summarization of the context. It allows us to control the size of the transmitted data, stabilizing both cost and response time. In the case of an asynchronous approach, the impact on latency is minimal. However, this comes at the cost of increased solution complexity and the risk of losing some information.</p>
<p>Next to our agent, we add a query that will be executed after every 10 turns with the user. It is worth noting that we trigger this mechanism after masking the observations, when we use the hybrid approach:</p>
<pre><code class="language-csharp">        public async Task&lt;string&gt; GetRecommendationAsync(string userInput)
        {
            ChatHistory.AddUserMessage(userInput);

            var result = await chatCompletionService.GetChatMessageContentAsync(
                ChatHistory,
                executionSettings: openAIPromptExecutionSettings,
                kernel: kernel);

            if (string.IsNullOrWhiteSpace(result.Content))
            {
                throw new InvalidOperationException($"{nameof(chatCompletionService)} did not return any content.");
            }

            ChatHistory.Add(result);

            if (useObservationMasking)
            {
                MaskObservations();
            }

            // Dodanie summarization
            if (useSummarization)
            {
                var numberOfTurnsWithUser = ChatHistory.Count(c =&gt; c.Role == AuthorRole.User);
                if (numberOfTurnsWithUser % 10 == 0)
                {
                    await SummarizeChatAsync();
                }
            }

            return result.Content;
        }
</code></pre>
<p>The summarization process can use a different, cheaper model than the main agent, because it does not require high-quality response generation, only information compression. In our case, for simplicity, we reuse Semantic Kernel. However, for this query, we send a separate <code>system prompt</code>:</p>
<pre><code class="language-csharp">        private const string SummarizationSystemPrompt = """
            You compress conversation transcripts for a product-recommendation assistant.
            Preserve concrete facts the user asked about, product names, prices, stock, and language the user used.
            Do not invent catalog data. Output a concise third-person summary suitable as context for continuing the chat.
            """;
</code></pre>
<p>we disable the ability to use tools:</p>
<pre><code class="language-csharp">        private readonly OpenAIPromptExecutionSettings summarizationPromptExecutionSettings = new()
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.None()
        };
</code></pre>
<p>and we also build a separate chat history as a transcript of the conversation so far between the agent and the user:</p>
<pre><code class="language-csharp">        private static string FormatConversationForSummary(ChatHistory history)
        {
            var blocks = new List&lt;string&gt;();
            foreach (var message in history)
            {
                if (message.Role == AuthorRole.System)
                {
                    continue;
                }

                var parts = new List&lt;string&gt;();
                if (!string.IsNullOrWhiteSpace(message.Content))
                {
                    parts.Add(message.Content.Trim());
                }

                foreach (var item in message.Items)
                {
                    switch (item)
                    {
                        case FunctionCallContent call:
                            parts.Add($"[called {call.PluginName}.{call.FunctionName} with arguments: {call.Arguments}]");
                            break;

                        case FunctionResultContent result:
                            parts.Add($"[result from {result.PluginName}.{result.FunctionName}: {FormatResultObject(result.Result)}]");
                            break;

                        case TextContent text when !string.IsNullOrWhiteSpace(text.Text):
                            parts.Add(text.Text.Trim());
                            break;
                    }
                }

                var body = parts.Count &gt; 0 ? string.Join(" ", parts) : "(no text)";
                blocks.Add($"{message.Role}: {body}");
            }

            return string.Join(Environment.NewLine + Environment.NewLine, blocks);
        }
</code></pre>
<p>Finally, we send a request for a summary, and from the response we create a new context for the AI agent, adding the original system prompt once again:</p>
<pre><code class="language-csharp">private async Task SummarizeChatAsync()
{
    var systemMessage = ChatHistory.FirstOrDefault(m =&gt; m.Role == AuthorRole.System);
    if (systemMessage is null)
    {
        return;
    }

    var transcript = FormatConversationForSummary(ChatHistory);
    if (string.IsNullOrWhiteSpace(transcript))
    {
        return;
    }

    var summarizationChat = new ChatHistory();
    summarizationChat.AddSystemMessage(SummarizationSystemPrompt);
    summarizationChat.AddUserMessage(
        "Summarize the following conversation transcript.\n\n" + transcript);

    var summaryResponse = await chatCompletionService.GetChatMessageContentAsync(
        summarizationChat,
        executionSettings: summarizationPromptExecutionSettings,
        kernel: null);

    if (string.IsNullOrWhiteSpace(summaryResponse.Content))
    {
        throw new InvalidOperationException("Summarization did not return any content.");
    }

    var shopSystemText = systemMessage.Content ?? string.Empty;
    ChatHistory.Clear();
    ChatHistory.AddSystemMessage(shopSystemText);
    ChatHistory.AddUserMessage("Summary of the conversation so far:\n" + summaryResponse.Content);
}
</code></pre>
<h2>Comparison of context management strategies based on a 21-turn conversation</h2>
<p>The prepared system should be tested to prove the effectiveness of token reduction mechanisms.</p>
<p>I prepared a 21-turn conversation, which I ran in 4 scenarios:</p>
<ul>
<li><p>without context management (Basic),</p>
</li>
<li><p>only observation masking (Omasking),</p>
</li>
<li><p>only LLM summarization (Summarization),</p>
</li>
<li><p>Observation masking and LLM summarization (OMask+Summ).</p>
</li>
</ul>
<p>These are all the questions (translated to English):</p>
<pre><code class="language-plaintext">1. List all products from the catalog with a short description of each.
2. I am looking for a laptop for programming and mobile work — what do you recommend and why?
3. I need something for gaming — which model makes sense and what are its key parameters?
4. Compare Laptop Pro 14 with DevBook 15 for a developer (.NET, VS / VS Code).
5. What is the exact technical description of the Creator Studio 16 model? List the specifications and use cases.
6. And Campus Note 14 — who is it for and how is it different from BalanceBook 14?
7. UltraLite 13 vs Silent Lite 13: which one is lighter or more “mobile” according to the catalog data?
8. Provide product details of Gaming Max 17 — CPU, RAM, display, weight if available in the description.
9. Do you have anything below a 14-inch screen? List models from the catalog that match.
10. Suggest a set: a laptop for studying at university and briefly justify each model in one sentence.
11. Let’s go back to Laptop Pro 14: repeat the most important advantages and limitations from the description.
12. Silent Lite 13 — full description with details; I am interested in the battery and display.
13. DevBook 15 — details from the catalog: what workloads is it suitable for and what is it not suitable for?
14. Does any laptop have a clear focus on silence or mobility in the description? Indicate which one and why.
15. I need something for photo/video (creator workflow) — what do you choose from the catalog and why?
16. BalanceBook 14 — full product details from the catalog.
17. Again, list all product names, but only names in one line, without descriptions.
18. Campus Note 14 — technical details and who it is for according to the full description.
19. If budget is not a concern: which one laptop would you recommend as “universal” and why?
20. Briefly compare three models: Gaming Max 17, Creator Studio 16, Laptop Pro 14 — who are they for.
21. Summarize our entire conversation: which models I considered and what you finally recommend.
</code></pre>
<p>I did not notice a significant difference in quality between the model’s responses in each scenario. For each of them, I measured the number of input tokens and total tokens for every interaction with the model and presented them on charts:</p>
<img src="https://cdn.hashnode.com/uploads/covers/670fba783947e083caa6f5b9/9c72ec65-2cf8-4e91-82d6-8f75f4aa7e3f.png" alt="" style="display:block;margin:0 auto" />

<h3>Example input tokens in different strategies</h3>
<p><strong>Absolute values:</strong></p>
<table>
<thead>
<tr>
<th>Experiment</th>
<th>1</th>
<th>10</th>
<th>11</th>
<th>20</th>
<th>21</th>
</tr>
</thead>
<tbody><tr>
<td>Basic</td>
<td>782</td>
<td>18652</td>
<td>19817</td>
<td>33657</td>
<td>17794</td>
</tr>
<tr>
<td>OMask+Summ</td>
<td>782</td>
<td>11902</td>
<td>2433</td>
<td>3005</td>
<td>1525</td>
</tr>
<tr>
<td>OMasking</td>
<td>782</td>
<td>11398</td>
<td>12045</td>
<td>21853</td>
<td>11055</td>
</tr>
<tr>
<td>Summarization</td>
<td>782</td>
<td>9651</td>
<td>1337</td>
<td>2357</td>
<td>2266</td>
</tr>
</tbody></table>
<p><strong>% compared to Basic:</strong></p>
<table>
<thead>
<tr>
<th>Experiment</th>
<th>1</th>
<th>10</th>
<th>11</th>
<th>20</th>
<th>21</th>
</tr>
</thead>
<tbody><tr>
<td>Basic</td>
<td>100.00%</td>
<td>100.00%</td>
<td>100.00%</td>
<td>100.00%</td>
<td>100.00%</td>
</tr>
<tr>
<td>OMask+Summ</td>
<td>100.00%</td>
<td>63.81%</td>
<td>12.28%</td>
<td>8.93%</td>
<td>8.57%</td>
</tr>
<tr>
<td>OMasking</td>
<td>100.00%</td>
<td>61.11%</td>
<td>60.78%</td>
<td>64.93%</td>
<td>62.13%</td>
</tr>
<tr>
<td>Summarization</td>
<td>100.00%</td>
<td>51.74%</td>
<td>6.75%</td>
<td>7.00%</td>
<td>12.73%</td>
</tr>
</tbody></table>
<p><strong>Reduction vs Basic:</strong></p>
<table>
<thead>
<tr>
<th>Experiment</th>
<th>1</th>
<th>10</th>
<th>11</th>
<th>20</th>
<th>21</th>
</tr>
</thead>
<tbody><tr>
<td>Basic</td>
<td>0.00%</td>
<td>0.00%</td>
<td>0.00%</td>
<td>0.00%</td>
<td>0.00%</td>
</tr>
<tr>
<td>OMask+Summ</td>
<td>0.00%</td>
<td>36.19%</td>
<td>87.72%</td>
<td>91.07%</td>
<td>91.43%</td>
</tr>
<tr>
<td>OMasking</td>
<td>0.00%</td>
<td>38.89%</td>
<td>39.22%</td>
<td>35.07%</td>
<td>37.87%</td>
</tr>
<tr>
<td>Summarization</td>
<td>0.00%</td>
<td>48.26%</td>
<td>93.25%</td>
<td>93.00%</td>
<td>87.27%</td>
</tr>
</tbody></table>
<p>As expected, applying observation masking led to a stable reduction in the number of input tokens at the level of about 35–40% compared to the baseline approach (except for the first turn).</p>
<p>In turn, LLM summarization caused a clear drop in the number of tokens after the 10th turn, reaching a reduction of around 93% in later interactions.</p>
<p>The hybrid approach combined both effects, providing a reduction in the number of tokens both before and after performing summarization.</p>
<h1>Conclusions</h1>
<p>The way AI agents manage context is an architectural decision that has a direct impact on cost, response time, quality, and scalability of the solution, and it should depend on the nature of the application.</p>
<p>Passing the full conversation history is not a scalable approach — as the interaction grows, the number of tokens increases, which leads to higher costs and longer response times without proportional improvement in quality.</p>
<p>Observation masking is a simple and effective mechanism for reducing tokens, which helps limit the “noise” coming from tool responses. However, it does not solve the problem of growing context in the long term.</p>
<p>LLM summarization makes it possible to control the context length by compressing it. In the analyzed scenario, it allowed reducing the number of tokens by over 90%, at the cost of an additional model call and potential loss of some information.</p>
<p>The best results are achieved with a hybrid approach, which combines both mechanisms — it reduces the number of tokens in every interaction (except the first one) and controls long-term context growth.</p>
<h3>Your turn</h3>
<p>How are you handling context growth in your AI agents?<br />Have you tried masking or summarization in production?</p>
<p>If you’re working with AI systems, feel free to share your approach — I’m always curious how others solve this.</p>
<p>And if this kind of content is useful to you, consider following for more articles on .NET, AI, and technical decisions.</p>
]]></content:encoded></item><item><title><![CDATA[Jak efektywnie zarządzać  kontekstem agentów AI w .NET]]></title><description><![CDATA[Wprowadzenie
Nowoczesne platformy do budowania agentów AI znacząco przyspieszają tworzenie takich rozwiązań. Pozwalają w prosty sposób wybrać duży model językowy (LLM), zarządzać promptami oraz defini]]></description><link>https://beniaminlenarcik.pl/jak-efektywnie-zarzadzac-kontekstem-agentow-ai-w-net</link><guid isPermaLink="true">https://beniaminlenarcik.pl/jak-efektywnie-zarzadzac-kontekstem-agentow-ai-w-net</guid><category><![CDATA[AI]]></category><category><![CDATA[agents]]></category><category><![CDATA[semantic kernel]]></category><category><![CDATA[context-window]]></category><category><![CDATA[context engineering]]></category><category><![CDATA[observation-masking]]></category><category><![CDATA[summarization]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Mon, 06 Apr 2026 22:21:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/670fba783947e083caa6f5b9/bd34972d-00af-4a3a-a860-cced15f45301.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1><strong>Wprowadzenie</strong></h1>
<p>Nowoczesne platformy do budowania agentów AI znacząco przyspieszają tworzenie takich rozwiązań. Pozwalają w prosty sposób wybrać duży model językowy (LLM), zarządzać promptami oraz definiować narzędzia (tools), z których agent może korzystać automatycznie.</p>
<p>Prawdziwym wyzwaniem po stronie programisty pozostaje jednak zapewnienie odpowiedniego kontekstu dla modelu. Frameworki takie jak <a href="https://docs.langchain.com/oss/python/langchain/messages">LangChain</a> w Pythonie czy <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/chat-history?pivots=programming-language-csharp">Semantic Kernel</a> w .NET oczekują, że przy każdym zapytaniu przekażemy historię rozmowy lub inne dane opisujące aktualny stan interakcji.</p>
<p>Intuicyjnie przekazywanie całej historii rozmowy za każdym razem do agenta wydaje się rozsądnym podejściem. Powinno to pozwolić agentowi lepiej rozumieć kontekst i podejmować trafniejsze decyzje. W praktyce pojawia się jednak kilka pytań:</p>
<ul>
<li><p>Czy większa liczba tokenów w promptcie zawsze prowadzi do lepszych odpowiedzi?</p>
</li>
<li><p>Jak nie przekroczyć limitu długości kontekstu poszczególnych modeli językowych?</p>
</li>
<li><p>Co zrobić z rosnącym czasem przetwarzania i kosztem kolejnych wywołań agenta?</p>
</li>
</ul>
<p>W tym artykule pokażę, jak zarządzać kontekstem przekazywanym do dużych modeli językowych na przykładach w .NET agenta zbudowanego na platformie Semantic Kernel. Omówię podejścia, które pozwalają kontrolować rozmiar kontekstu, a tym samym zapewnić stabilność kosztów oraz jakość działania rozwiązań opartych na agentach AI.</p>
<h1>Context window – podstawowe ograniczenie LLM</h1>
<p>Modele językowe mają ograniczenie na maksymalny rozmiar kontekstu nazywane <strong>context window</strong>. Obejmuje ono wszystkie tokeny używane podczas jednego wywołania modelu — zarówno wejściowe, jak i wyjściowe. W zależności od modelu limit to np. 128k tokenów (<a href="https://developers.openai.com/api/docs/models/gpt-5-chat-latest">OpenAI GPT-5</a>).</p>
<p>W praktyce, przy budowie agentów AI oznacza to, że w tym limicie muszą zmieścić się między innymi:</p>
<ul>
<li><p>prompt systemowy,</p>
</li>
<li><p>historia rozmowy,</p>
</li>
<li><p>wyniki narzędzi (tool outputs),</p>
</li>
<li><p>nowy kontekst dostarczony do agenta, np. pytanie użytkownika lub zdarzenie z systemu.</p>
</li>
</ul>
<p>Ponieważ context window obejmuje również tokeny wyjściowe, zbyt dużo danych na wejściu może spowodować, że model będzie miał mniej przestrzeni na proces wnioskowania (reasoning) oraz wygenerowanie odpowiedzi.</p>
<h1>Czy więcej kontekstu zawsze pomaga?</h1>
<p>Intuicyjnie więcej kontekstu powinno poprawiać wyniki. W praktyce nie zawsze tak jest. W pracy <a href="https://arxiv.org/abs/2307.03172"><em>Lost in the Middle: How Language Models Use Long Contexts</em></a> pokazano, że zwiększanie liczby dokumentów w promptcie przestaje poprawiać jakość odpowiedzi mimo rosnącej liczby trafnych danych. Dodatkowo modele najlepiej wykorzystują informacje z początku i końca kontekstu, a najgorzej te znajdujące się w jego środku (tzw. <em>Lost in the Middle</em>).</p>
<h1>Jak zarządzać kontekstem agentów AI</h1>
<p>Skoro większy kontekst nie zawsze prowadzi do lepszych wyników, pojawia się naturalne pytanie - <strong>jak zarządzać kontekstem agentów AI?</strong></p>
<p>Temat ten został szerzej opisany w artykule <a href="https://blog.jetbrains.com/research/2025/12/efficient-context-management/">Cutting Through the Noise: Smarter Context Management for LLM-Powered Agents</a> dotyczącym efektywnego zarządzania kontekstem w systemach opartych na LLM. W praktyce stosuje się dwa główne podejścia:</p>
<ul>
<li><p><strong>LLM summarization</strong> – kompresowanie historii do krótszej formy</p>
</li>
<li><p><strong>observation masking</strong> – ograniczanie liczby przekazywanych danych</p>
</li>
</ul>
<p>Każde z nich ma swoje trade-offy: summarization lepiej kontroluje rozmiar kontekstu, ale generuje dodatkowy koszt i może gubić szczegóły. Masking jest prostszy i tańszy, ale nie zapobiega nieograniczonemu wzrostowi kontekstu. W praktyce najlepsze rezultaty daje podejście hybrydowe, łączące oba mechanizmy. W kolejnych sekcjach pokażę, jak zaimplementować je przy użyciu Semantic Kernel w .NET.</p>
<h1>Zarządzanie kontekstem agentów AI w Semantic Kernel .NET</h1>
<h2>Agent AI rekomendujący produkty</h2>
<p>Wyobraźmy sobie sklep internetowy ze sprzętem komputerowym, w którym agent AI pomaga użytkownikowi wybrać laptop na podstawie preferencji, takich jak budżet, waga, czas pracy baterii czy zastosowanie. Agent, krok po kroku zbiera wymagania użytkownika, wyszukuje pasujące produkty, analizuje ich opisy i rekomenduje sprzęt.</p>
<p>Na początku tworzymy podstawowego agenta, a następnie implementujemy strategie zarządzania kontekstem - observation masking oraz LLM summarization. Przeanalizujemy ich wpływ na liczbę zużytych tokenów wejścia (input tokens) i tokenów całościowych (total tokens) na podstawie dodatkowych mechanizmów analitycznych zbudowanych w ramach projektu.</p>
<p>Kod źródłowy, którego fragmenty znajdują się w artykule dostępny jest w repozytorium GitHub <a href="https://github.com/L3mur1/SemanticKernelContextManagement">SemanticKernelContextManagement</a>.</p>
<h2>Podstawowy agent AI</h2>
<p>Do stworzenia agenta w .NET wykorzystamy framework Semantic Kernel, którego przykład typu "Hello World" łatwo zbudować na podstawie oficjalnej <a href="https://learn.microsoft.com/en-us/semantic-kernel/get-started/quick-start-guide?pivots=programming-language-csharp">dokumentacji</a>. Na potrzeby eksperymentu wyposażymy go w <a href="https://github.com/L3mur1/SemanticKernelContextManagement/blob/master/SemanticKernelContextManagement/Products/SemanticsKernel/ProductsPlugin.cs">ProductsPlugin</a>, który zwróci informacje o laptopach, które dla ułatwienia będą zawarte w projekcie w statycznych plikach JSON i wczytane do pamięci programu. Do rekomendacji:</p>
<ul>
<li><p>Ogólnej, która zwraca skrócone informacje o wszystkich laptopach:</p>
<pre><code class="language-csharp">        [KernelFunction]
        [Description("Get all products from shop. Returns only product names and short summaries.")]
        public string GetProducts()
        {
            var productSummaries = products.Select(p =&gt; new
            {
                p.Name,
                p.ShortSummary,
            });

            return JsonSerializer.Serialize(productSummaries, ProductJsonOptions);
        }
</code></pre>
</li>
<li><p>Szczegółowej, dotyczącej jednego produktu (laptopa):</p>
</li>
</ul>
<pre><code class="language-csharp">        [Description("Get full detailed description of a product by its name.")]
        public string GetProductDetails([Description("Product name, e.g. Laptop Pro 14")] string productName)
        {
            var product = products.FirstOrDefault(p =&gt; p.Name.Equals(productName, StringComparison.OrdinalIgnoreCase));
            if (product == null)
            {
                return $"Product not found: {productName}";
            }

            return JsonSerializer.Serialize(product, ProductJsonOptions);
        }
</code></pre>
<p>W pierwszym zapytaniu przekazujemy dodatkowo system prompt, który instruuje, że model ma się wcielić w rolę asystenta w sklepie. Z zalet warto zwrócić uwagę, że asystent zawsze odpowie w języku rozmówcy 🙂:</p>
<pre><code class="language-csharp">        private const string SystemPrompt = """
            You are a shop assistant that recommends products from our catalog.
            Use the Products plugin (GetProducts, GetProductDetails) to read real catalog data before suggesting items.
            Do not invent products, prices, or stock. If nothing matches, say so clearly.
            Match the user's language in your replies.
            """;
</code></pre>
<p>W Semantic Kernel możemy zdecydować, czy agent powinien skorzystać ze zdefiniowanych narzędzi w plugin. W tym wypadku użyjemy <a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/function-calling/function-choice-behaviors?pivots=programming-language-csharp">FunctionChoiceBehavior.Auto</a> co oznacza, że może skorzystać z funkcji, ale nie musi:</p>
<pre><code class="language-csharp">        private readonly OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
        {
            FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
        };
</code></pre>
<p>Program tworzymy jako aplikację konsolową. Z konsoli pobieramy zapytanie klienta sklepu, przykładowo "Chcę kupić laptopa". W podstawowym podejściu dodajemy pytanie do kontekstu jako <code>UserMessage</code> i wysyłamy całość do LLM. Po otrzymaniu odpowiedzi zapisujemy ją również w historii i zwracamy do użytkownika:</p>
<pre><code class="language-csharp">        public async Task&lt;string&gt; GetRecommendationAsync(string userInput)
        {
            ChatHistory.AddUserMessage(userInput);

            var result = await chatCompletionService.GetChatMessageContentAsync(
                ChatHistory,
                executionSettings: openAIPromptExecutionSettings,
                kernel: kernel);

            if (string.IsNullOrWhiteSpace(result.Content))
            {
                throw new InvalidOperationException($"{nameof(chatCompletionService)} did not return any content.");
            }

            ChatHistory.Add(result);
            return result.Content;
        }
</code></pre>
<p>Przykładowy dialog w konsoli:</p>
<img src="https://cdn.hashnode.com/uploads/covers/670fba783947e083caa6f5b9/c14ff1be-fe60-46f1-8ebc-2608a8fdca3f.png" alt="" style="display:block;margin:0 auto" />

<h2>Maskowanie obserwacji (observation masking)</h2>
<p>W agentach zbudowanych na platformie Semantic Kernel surowe wyniki narzędzi trafiają jeden pod drugim do historii rozmowy jako wiadomości z przypisaną rolą <a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.semantickernel.chatcompletion.authorrole.tool?view=semantic-kernel-dotnet#microsoft-semantickernel-chatcompletion-authorrole-tool">AuthorRole.Tool</a>. Nie są one widoczne dla klienta naszego sklepu. Zwracamy użytkownikowi przetworzoną przez agenta odpowiedź, wygenerowaną właśnie na podstawie tych wyników.</p>
<p>W przypadku funkcji <code>GetProductDetails</code> wynikiem jest JSON zawierający pełne informacje o konkretnym laptopie. Załóżmy, że w większości przypadków użycia te same informacje o produkcie trafią do wygenerowanej przez asystenta odpowiedzi. Podejmujemy świadomą decyzję o wprowadzeniu maskowania obserwacji. W ten sposób zmniejszamy koszty i czas oczekiwania na odpowiedź w zamian za obniżenie jakości - większe ryzyko halucynacji oraz ewentualne zwiększenie ilości ponownych wywołań narzędzi.</p>
<p>Maskujemy wyniki narzędzi przez wprowadzenie w ich miejsce placeholdera:</p>
<pre><code class="language-csharp">private const string ObservationMaskedPlaceholder = "[TOOL_OBSERVATION_MASKED]";
</code></pre>
<p>Mechanizm wprowadzamy, tuż przed zwróceniem odpowiedzi do klienta, tak by zadziałał on dla kolejnego zapytania użytkownika:</p>
<pre><code class="language-csharp">        public async Task&lt;string&gt; GetRecommendationAsync(string userInput)
        {
            ChatHistory.AddUserMessage(userInput);

            var result = await chatCompletionService.GetChatMessageContentAsync(
                ChatHistory,
                executionSettings: openAIPromptExecutionSettings,
                kernel: kernel);

            if (string.IsNullOrWhiteSpace(result.Content))
            {
                throw new InvalidOperationException($"{nameof(chatCompletionService)} did not return any content.");
            }

            ChatHistory.Add(result);

            // maskowanie obserwacji
            if (useObservationMasking)
            {
                MaskObservations();
            }

            return result.Content;
        }
</code></pre>
<pre><code class="language-csharp">        private void MaskObservations()
        {
            var toolMessages = ChatHistory.Where(m =&gt; m.Role == AuthorRole.Tool);
            foreach (var message in toolMessages)
            {
                message.Content = ObservationMaskedPlaceholder;

                if (message.Items.Count == 0)
                {
                    message.Items.Add(new TextContent(ObservationMaskedPlaceholder));
                    continue;
                }

                var originals = message.Items.ToArray();
                message.Items.Clear();

                foreach (var item in originals)
                {
                    if (item is FunctionResultContent functionResult)
                    {
                        message.Items.Add(new FunctionResultContent(
                            functionResult.FunctionName,
                            functionResult.PluginName,
                            functionResult.CallId,
                            ObservationMaskedPlaceholder));
                    }
                    else
                    {
                        message.Items.Add(new TextContent(ObservationMaskedPlaceholder));
                    }
                }
            }
        }
</code></pre>
<h2>Podsumowywanie kontekstu przez LLM (LLM summarization)</h2>
<p>Wyobraźmy sobie, że klienci naszego sklepu zadają wiele pytań o laptopy w trakcie jednej rozmowy. Wraz z rosnącą historią interakcji zwiększa się liczba tokenów przekazywanych do modelu. W efekcie kolejne odpowiedzi są generowane coraz wolniej, koszt ich przetwarzania rośnie liniowo, a jakość odpowiedzi nie ulega istotnej poprawie. W tej sytuacji możemy świadomie wprowadzić okresowe podsumowywanie przez AI kontekstu. Pozwala ono kontrolować rozmiar przekazywanych danych, stabilizując koszt i czas odpowiedzi. W przypadku podejścia asynchronicznego wpływ na latency jest minimalny. Odbywa się to jednak kosztem większej złożoności rozwiązania oraz ryzyka utraty części informacji.</p>
<p>Obok naszego agenta dodajemy zapytanie, które będzie wykonywane po każdych 10 turach z użytkownikiem. Warto zaznaczyć, że mechanizm wywołujemy już po zamaskowaniu obserwacji, gdy korzystamy z podejścia hybrydowego:</p>
<pre><code class="language-csharp">        public async Task&lt;string&gt; GetRecommendationAsync(string userInput)
        {
            ChatHistory.AddUserMessage(userInput);

            var result = await chatCompletionService.GetChatMessageContentAsync(
                ChatHistory,
                executionSettings: openAIPromptExecutionSettings,
                kernel: kernel);

            if (string.IsNullOrWhiteSpace(result.Content))
            {
                throw new InvalidOperationException($"{nameof(chatCompletionService)} did not return any content.");
            }

            ChatHistory.Add(result);

            if (useObservationMasking)
            {
                MaskObservations();
            }

            // Dodanie summarization
            if (useSummarization)
            {
                var numberOfTurnsWithUser = ChatHistory.Count(c =&gt; c.Role == AuthorRole.User);
                if (numberOfTurnsWithUser % 10 == 0)
                {
                    await SummarizeChatAsync();
                }
            }

            return result.Content;
        }
</code></pre>
<p>Proces podsumowywania może korzystać z innego, tańszego modelu niż główny agent, ponieważ nie wymaga on wysokiej jakości generowania odpowiedzi, a jedynie kompresji informacji. W naszym przypadku dla ułatwienia reużywamy Semantic Kernel. Jednak dla tego zapytania wysyłamy osobny <code>system prompt</code>:</p>
<pre><code class="language-csharp">private const string SummarizationSystemPrompt = """
    You compress conversation transcripts for a product-recommendation assistant.
    Preserve concrete facts the user asked about, product names, prices, stock, and language the user used.
    Do not invent catalog data. Output a concise third-person summary suitable as context for continuing the chat.
    """;
</code></pre>
<p>wyłączamy możliwość korzystania z <code>tools</code>:</p>
<pre><code class="language-csharp">   private readonly OpenAIPromptExecutionSettings summarizationPromptExecutionSettings = new()
{
    FunctionChoiceBehavior = FunctionChoiceBehavior.None()
};
</code></pre>
<p>oraz budujemy osobne <code>chat history</code> jako transkrypt z dotychczasowej rozmowy między agentem, a użytkownikiem:</p>
<pre><code class="language-csharp">private string FormatConversationForSummary()
{
    var blocks = new List&lt;string&gt;();
    foreach (var message in ChatHistory)
    {
        if (message.Role == AuthorRole.System)
        {
            continue;
        }

        var parts = new List&lt;string&gt;();
        if (!string.IsNullOrWhiteSpace(message.Content))
        {
            parts.Add(message.Content.Trim());
        }

        foreach (var item in message.Items)
        {
            switch (item)
            {
                case FunctionCallContent call:
                    parts.Add($"[called {call.PluginName}.{call.FunctionName} with arguments: {call.Arguments}]");
                    break;

                case FunctionResultContent result:
                    parts.Add($"[result from {result.PluginName}.{result.FunctionName}: {FormatResultObject(result.Result)}]");
                    break;

                case TextContent text when !string.IsNullOrWhiteSpace(text.Text):
                    parts.Add(text.Text.Trim());
                    break;
            }
        }

        var body = parts.Count &gt; 0 ? string.Join(" ", parts) : "(no text)";
        blocks.Add($"{message.Role}: {body}");
    }

    return string.Join(Environment.NewLine + Environment.NewLine, blocks);
}
</code></pre>
<p>Ostatecznie wysyłamy request o summary, i z odpowiedzi tworzymy nowy kontekst dla agenta AI, dodając jeszcze raz oryginalny system prompt.</p>
<pre><code class="language-csharp">private async Task SummarizeChatAsync()
{
    var transcript = FormatConversationForSummary();

    var summarizationChat = new ChatHistory();
    summarizationChat.AddSystemMessage(SummarizationSystemPrompt);
    summarizationChat.AddUserMessage(
        "Summarize the following conversation transcript.\n\n" + transcript);

    var summaryResponse = await chatCompletionService.GetChatMessageContentAsync(
        summarizationChat,
        executionSettings: summarizationPromptExecutionSettings,
        kernel: null);

    if (string.IsNullOrWhiteSpace(summaryResponse.Content))
    {
        throw new InvalidOperationException("Summarization did not return any content.");
    }

    NotifySummarizationTokenUsed(summaryResponse);

    ChatHistory.Clear();
    ChatHistory.AddSystemMessage(SystemPrompt);
    ChatHistory.AddUserMessage("Summary of the conversation so far:\n" + summaryResponse.Content);
}
</code></pre>
<h2>Porównanie strategii zarządzania kontekstem na przykładzie 21-turowej rozmowy</h2>
<p>Przygotowany system należy przetestować, by udowodnić skuteczność mechanizmów redukcji tokenów.</p>
<p>Przygotowałem 21-turową rozmowę, którą przeprowadziłem w 4 scenariuszach:</p>
<ol>
<li><p>Bez zarządzania kontekstem (Basic).</p>
</li>
<li><p>Tylko maskowanie obserwacji (Omasking).</p>
</li>
<li><p>Wyłącznie LLM summarization (Summarization).</p>
</li>
<li><p>Maskowanie obserwacji oraz LLM Summarization (OMask+Summ).</p>
</li>
</ol>
<p>To wszystkie pytania:</p>
<pre><code class="language-plaintext">1. Wypisz wszystkie produkty z katalogu z krótkim opisem każdego.
2. Szukam laptopa pod programowanie i pracę mobilną — co polecasz i dlaczego?
3. Potrzebuję czegoś do gier — który model ma sens i jakie ma kluczowe parametry?
4. Porównaj Laptop Pro 14 z DevBook 15 pod kątem developera (.NET, VS / VS Code).
5. Jaki jest dokładny opis techniczny modelu Creator Studio 16? Wypisz specyfikację i zastosowania.
6. A Campus Note 14 — dla kogo jest i czym różni się od BalanceBook 14?
7. UltraLite 13 vs Silent Lite 13: który jest lżejszy lub bardziej „mobilny” według danych z katalogu?
8. Podaj szczegóły produktu Gaming Max 17 — CPU, RAM, ekran, wagę jeśli są w opisie.
9. Czy masz coś poniżej 14 cali ekranu? Wymień modele z katalogu, które pasują.
10. Zaproponuj zestaw: laptop do nauki na uczelnię i krótko uzasadnij jednym zdaniem na model.
11. Wróćmy do Laptop Pro 14: powtórz najważniejsze zalety i ograniczenia z opisu.
12. Silent Lite 13 — pełny opis z szczegółami; interesuje mnie bateria i wyświetlacz.
13. DevBook 15 — szczegóły z katalogu: dla jakich obciążeń się nadaje, a dla jakich nie?
14. Czy któryś laptop ma wyraźny nacisk na ciszę lub mobilność w opisie? Wskaż który i dlaczego.
15. Potrzebuję czegoś do photo/video (creator workflow) — co wybierasz z katalogu i dlaczego?
16. BalanceBook 14 — pełne szczegóły produktu z katalogu.
17. Znowu lista wszystkich nazw produktów, ale tylko nazwy w jednej linii, bez opisów.
18. Campus Note 14 — szczegóły techniczne i dla kogo jest według długiego opisu.
19. Jeśli budżet nie gra roli: jaki jeden laptop byś polecił „universal” i dlaczego?
20. Porównaj krótko trzy modele: Gaming Max 17, Creator Studio 16, Laptop Pro 14 — kto dla kogo.
21. Podsumuj całą naszą rozmowę: jakie modele brałem pod uwagę i co finalnie rekomendujesz.
</code></pre>
<p>Nie zauważyłem znaczącej różnicy jakości między odpowiedziami modelu w każdym scenariuszu. Dla każdego z nich zmierzyłem ilość tokenów wejściowych (input tokens) oraz total tokens każdej interakcji z modelem i przedstawiłem na wykresach:</p>
<img src="https://cdn.hashnode.com/uploads/covers/670fba783947e083caa6f5b9/c843c3ff-8a0b-4c12-b799-1812e734c198.png" alt="" style="display:block;margin:0 auto" />

<h3>Przykładowe input tokens w poszczególnych strategiach</h3>
<p>Wartości bezwzględne:</p>
<table>
<thead>
<tr>
<th>Strategy/Turn</th>
<th>1</th>
<th>10</th>
<th>11</th>
<th>20</th>
<th>21</th>
</tr>
</thead>
<tbody><tr>
<td>Basic</td>
<td>782</td>
<td>18652</td>
<td>19817</td>
<td>33657</td>
<td>17794</td>
</tr>
<tr>
<td>OMask+Summ</td>
<td>782</td>
<td>11902</td>
<td>2433</td>
<td>3005</td>
<td>1525</td>
</tr>
<tr>
<td>OMasking</td>
<td>782</td>
<td>11398</td>
<td>12045</td>
<td>21853</td>
<td>11055</td>
</tr>
<tr>
<td>Summarization</td>
<td>782</td>
<td>9651</td>
<td>1337</td>
<td>2357</td>
<td>2266</td>
</tr>
</tbody></table>
<p>% względem Basic:</p>
<table>
<thead>
<tr>
<th>Strategy/Turn</th>
<th>1</th>
<th>10</th>
<th>11</th>
<th>20</th>
<th>21</th>
</tr>
</thead>
<tbody><tr>
<td>Basic</td>
<td>100.00%</td>
<td>100.00%</td>
<td>100.00%</td>
<td>100.00%</td>
<td>100.00%</td>
</tr>
<tr>
<td>OMask+Summ</td>
<td>100.00%</td>
<td>63.81%</td>
<td>12.28%</td>
<td>8.93%</td>
<td>8.57%</td>
</tr>
<tr>
<td>OMasking</td>
<td>100.00%</td>
<td>61.11%</td>
<td>60.78%</td>
<td>64.93%</td>
<td>62.13%</td>
</tr>
<tr>
<td>Summarization</td>
<td>100.00%</td>
<td>51.74%</td>
<td>6.75%</td>
<td>7.00%</td>
<td>12.73%</td>
</tr>
</tbody></table>
<p>Redukcja vs Basic:</p>
<table>
<thead>
<tr>
<th>Strategy/Turn</th>
<th>1</th>
<th>10</th>
<th>11</th>
<th>20</th>
<th>21</th>
</tr>
</thead>
<tbody><tr>
<td>Basic</td>
<td>0.00%</td>
<td>0.00%</td>
<td>0.00%</td>
<td>0.00%</td>
<td>0.00%</td>
</tr>
<tr>
<td>OMask+Summ</td>
<td>0.00%</td>
<td>36.19%</td>
<td>87.72%</td>
<td>91.07%</td>
<td>91.43%</td>
</tr>
<tr>
<td>OMasking</td>
<td>0.00%</td>
<td>38.89%</td>
<td>39.22%</td>
<td>35.07%</td>
<td>37.87%</td>
</tr>
<tr>
<td>Summarization</td>
<td>0.00%</td>
<td>48.26%</td>
<td>93.25%</td>
<td>93.00%</td>
<td>87.27%</td>
</tr>
</tbody></table>
<p>Zgodnie z oczekiwaniami zastosowanie maskowania obserwacji doprowadziło do stabilnej redukcji liczby tokenów wejściowych na poziomie około 35–40% względem podejścia bazowego (poza pierwszą turą).</p>
<p>Z kolei LLM summarization spowodowało wyraźny spadek liczby tokenów po 10 turze, osiągając redukcję rzędu 93% w dalszych interakcjach.</p>
<p>Podejście hybrydowe połączyło oba efekty, zapewniając redukcję liczby tokenów zarówno przed, jak i po wykonaniu summarization.</p>
<h1>Wnioski</h1>
<p>Sposób zarządzania kontekstem agentów AI jest decyzją architektoniczną, która ma bezpośredni wpływ na koszt, czas odpowiedzi, jakość oraz skalowalność rozwiązania i powinien być uzależniony od charakteru aplikacji.</p>
<p>Przekazywanie pełnej historii rozmowy nie jest podejściem skalowalnym — wraz z długością interakcji rośnie liczba tokenów, co prowadzi do zwiększenia kosztów i czasu odpowiedzi bez proporcjonalnej poprawy jakości.</p>
<p>Observation masking stanowi prosty i efektywny mechanizm redukcji tokenów, który pozwala ograniczyć „szum” wynikający z odpowiedzi narzędzi. Nie rozwiązuje jednak problemu rosnącego kontekstu w dłuższej perspektywie.</p>
<p>LLM summarization umożliwia kontrolowanie długości kontekstu poprzez jego kompresję. W badanym scenariuszu pozwoliło to osiągnąć redukcję liczby tokenów nawet o ponad 90%, kosztem dodatkowego wywołania modelu oraz potencjalnej utraty części informacji.</p>
<p>Najlepsze rezultaty osiąga podejście hybrydowe, które łączy oba mechanizmy — redukuje liczbę tokenów w każdej interakcji (poza pierwszą) oraz kontroluje długoterminowy wzrost kontekstu.</p>
]]></content:encoded></item><item><title><![CDATA[Klucze naturalne w CosmosDB: szybsze odczyty i prostsze integracje]]></title><description><![CDATA[Wprowadzenie
Identyfikatory obiektów (ID) odgrywają kluczową rolę w systemach backendowych i rozproszonych. Zwykle do identyfikacji stosujemy klucze zastępcze (surrogate keys) – generowane losowo (np. GUID) lub sekwencyjnie w bazie danych. Istnieje j...]]></description><link>https://beniaminlenarcik.pl/klucze-naturalne-w-cosmosdb-szybsze-odczyty-i-prostsze-integracje</link><guid isPermaLink="true">https://beniaminlenarcik.pl/klucze-naturalne-w-cosmosdb-szybsze-odczyty-i-prostsze-integracje</guid><category><![CDATA[natural keys]]></category><category><![CDATA[CosmosDB]]></category><category><![CDATA[cosmos]]></category><category><![CDATA[Azure]]></category><category><![CDATA[.NET]]></category><category><![CDATA[idempotency]]></category><category><![CDATA[efficiency]]></category><category><![CDATA[improvement]]></category><category><![CDATA[optimization]]></category><category><![CDATA[Hashing]]></category><category><![CDATA[hashfunctions]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Tue, 30 Sep 2025 12:02:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759240765148/fbfcdc8e-2710-4c64-abb0-c67d16054c12.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-wprowadzenie">Wprowadzenie</h1>
<p>Identyfikatory obiektów (ID) odgrywają kluczową rolę w systemach backendowych i rozproszonych. Zwykle do identyfikacji stosujemy klucze zastępcze (surrogate keys) – generowane losowo (np. GUID) lub sekwencyjnie w bazie danych. Istnieje jednak inne podejście, użycie kluczy naturalnych. W tym przypadku id wyliczane są w przewidywalny sposób na podstawie danych biznesowych.</p>
<p>W artykule pokażę, w jakich sytuacjach zastosowanie deterministycznych identyfikatorów może przyspieszyć odczyty i uprościć integracje w naszych aplikacjach.</p>
<h1 id="heading-jak-deterministyczne-id-upraszczaja-prace-z-cosmosdb">Jak deterministyczne ID upraszczają pracę z CosmosDB</h1>
<h2 id="heading-korzysci-przy-odczytach">Korzyści przy odczytach</h2>
<p>Jeśli generowanie klucza w bazie danych odbywa się po stronie klienta, największym ryzykiem jest duplikacja. W praktyce często stosujemy <code>Guid.NewGuid()</code>, co redukuje prawdopodobieństwo kolizji niemal do zera. W bardziej złożonych scenariuszach zdarza się jednak, że powołujemy osobne serwisy do generowania kluczy.</p>
<p>W przypadku pracy z bezserwerową bazą danych NoSQL <strong>Azure Cosmos DB</strong> sytuacja wygląda inaczej. Pole <code>id</code> nie jest globalnie unikalne – musi być niepowtarzalne tylko w obrębie jednej partycji. Prawdziwa tożsamość dokumentu to para <code>(partitionKey, id)</code>. Jeśli zaprojektujemy ją w przewidywalny sposób, zyskamy prostsze i szybsze operacje.</p>
<p>Przykładem może być system fakturowy w sklepie, w którym kluczem partycji jest identyfikator klienta, a <code>id</code> to ten sam identyfikator rozszerzony o numer zamówienia. Dokument o numerze <code>C123-2025/09/001</code> w partycji klienta <code>C123</code> ma wtedy tożsamość: <code>partitionKey = "C123", id = "C123-2025/09/001"</code>. Wyszukanie faktury dla konkretnego zamówienia sprowadza się wówczas do jednoznacznego odczytu o złożoności <strong>O(1)</strong>. W rozwiązaniu wykorzystującym klucz zastępczy należałoby wykonać zapytanie:</p>
<pre><code class="lang-sql"><span class="hljs-comment">/*/ Przykładowe query w języku NoSQL używanym w CosmosDB /*/</span>
<span class="hljs-keyword">SELECT</span> TOP <span class="hljs-number">1</span> c
<span class="hljs-keyword">FROM</span> c
<span class="hljs-keyword">WHERE</span> c.orderNumber = <span class="hljs-string">"C123-2025/09/001"</span>
</code></pre>
<p>Oznacza to przeszukiwanie całej partycji to jest przejście przez nią jeden raz aż do znalezienia wyniku. Złożonośc takiego algorytmu to O(n).</p>
<h2 id="heading-korzysci-przy-zapisie">Korzyści przy zapisie</h2>
<p>Zalety stosowania kluczy naturalnych widać także przy zapisie. Wyobraźmy sobie, że faktury przechodzą w aplikacji kilka etapów – od szkicu po wersję finalną.</p>
<ul>
<li><p>Przy korzystaniu z identyfikatorów <code>Guid.NewGuid()</code> trzeba najpierw wykonać krok A, czyli wygenerować identyfikator albo odczytać go z bazy, a dopiero potem przekazać dalej.</p>
</li>
<li><p>Dodanie tej samej faktury nie jest wtedy idempotentne. Zabezpieczenie przed duplikatami wymaga dodatkowych odczytów i logiki w kodzie.</p>
</li>
</ul>
<p>Stosując deterministyczne identyfikatory możemy od razu uruchamiać równolegle różne procesy korzystające z tego samego klucza. Dzięki temu całość działa szybciej i bardziej niezależnie. Unikamy duplikatów, a kod staje się prostszy.</p>
<h2 id="heading-kiedy-nie-stosowac-kluczy-naturalnych">Kiedy nie stosować kluczy naturalnych</h2>
<p>Nie każde pole biznesowe nadaje się do roli identyfikatora:</p>
<ul>
<li><p><strong>Adres e-mail użytkownika</strong> <strong>czy nazwa produktu</strong> – to pola mutowalne, które w naturalny sposób ulegają zmianom.</p>
</li>
<li><p><strong>PESEL i inne dane wrażliwe</strong> – nie powinny być używane jako klucze ani powielane w bazie bez uzasadnienia.</p>
</li>
</ul>
<p>Użycie takich pól jako kluczy prowadziłoby do kolizji, problemów migracyjnych oraz ryzyka niezamierzonej ekspozycji danych wrażliwych (np. w logach, adresach URL czy systemach integracyjnych). Klucze naturalne warto więc opierać wyłącznie na polach faktycznie unikalnych, niemutowalnych i neutralnych pod względem bezpieczeństwa, np. numerach faktur, przesyłek czy kodach referencyjnych generowanych w systemie źródłowym.</p>
<h1 id="heading-klucze-naturalne-w-bazach-relacyjnych-sql">Klucze naturalne w bazach relacyjnych (SQL)</h1>
<p>W klasycznych bazach relacyjnych (SQL Server, PostgreSQL, MySQL) również spotykamy się z deterministycznymi kluczami, choć częściej mówi się o nich jako o <strong>kluczach naturalnych</strong>. Przykładami mogą być numery faktur, kody ISBN czy numery VIN. Są to pola, które jednoznacznie identyfikują rekord i nie zmieniają się w czasie.</p>
<p>Alternatywą są <strong>klucze techniczne</strong> (np. <code>INT IDENTITY</code> albo <code>GUID</code>), które są generowane niezależnie od logiki biznesowej. W takim podejściu zwykle dodatkowo zabezpiecza się pola biznesowe przez indeksy <code>UNIQUE</code>, aby zapobiec duplikatom.</p>
<p>W SQL wybór między kluczem naturalnym a technicznym jest głównie decyzją modelowania danych i zarządzania unikalnością, podczas gdy w Cosmos DB deterministyczne ID mają bezpośredni wpływ także na wydajność i koszt operacji (O(1) odczyty zamiast pełnych skanów partycji).</p>
<h1 id="heading-jak-tworzyc-deterministyczne-id">Jak tworzyć deterministyczne ID</h1>
<h2 id="heading-konkatenacja-pol-biznesowych"><strong>Konkatenacja</strong> <strong>pól biznesowych</strong></h2>
<p>Najprostsza metoda tworzenia deterministycznych identyfikatorów to konkatenacja pól biznesowych. Identyfikator powstaje z połączenia kilku unikalnych atrybutów, np.:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span> <span class="hljs-title">GetKeyOrder</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> customerId, <span class="hljs-keyword">string</span> orderNumber</span>)</span>
    =&gt; <span class="hljs-string">$"<span class="hljs-subst">{customerId}</span>:<span class="hljs-subst">{orderNumber}</span>"</span>;
</code></pre>
<p>Takie rozwiązanie jest wyjątkowo proste i czytelne – już po samym ID można rozpoznać, do czego się odnosi. Ma jednak i słabe strony. Długość identyfikatora zależy bezpośrednio od wartości pól, więc nie ma gwarancji stałego formatu i długości. Trzeba też uważać na separatory i sposób łączenia danych, aby uniknąć kolizji lub niejednoznaczności.</p>
<h2 id="heading-uzycie-funkcji-skrotu-hash-function">Użycie funkcji skrótu (hash function)</h2>
<p>Drugim podejściem jest <strong>wyliczanie hasha z kluczy</strong>. Zamiast przechowywać całe wartości, można je złączyć i przepuścić przez funkcję skrótu (hash function):</p>
<pre><code class="lang-sql">public static string HashFrom(params string[] parts)
{
    using var sha = SHA256.Create();
    var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(string.Join("|", parts)));
    return Convert.ToHexString(bytes);
}
</code></pre>
<p>Hash ma zawsze stałą długość i nie ujawnia wprost danych biznesowych. Dzięki temu łatwiej go przechowywać, indeksować i przenosić między systemami. Jest to podejście stosowane m.in. w <strong>Git</strong>, gdzie identyfikator commita powstaje właśnie jako hash (<code>SHA-1</code> lub <code>SHA-256</code>) obliczony z treści commita i metadanych. Dzięki temu commit o tej samej zawartości zawsze ma ten sam identyfikator, a repozytoria mogą łatwo synchronizować dane i wykrywać duplikaty. Z drugiej strony takie ID jest trudniejsze do odczytania przez człowieka, a w teorii istnieje niewielkie ryzyko kolizji.</p>
<p>Przy wyborze algorytmu haszowania warto brać pod uwagę nie tylko kwestie bezpieczeństwa, lecz także skalę danych. W mniejszych zbiorach wystarczą szybsze i prostsze algorytmy, które zapewniają akceptowalnie małe ryzyko kolizji. Przy dużych wolumenach danych lepiej postawić na silniejsze funkcje, takie jak <code>SHA-256</code> czy <code>SHA-512</code>, które minimalizują ryzyko kolizji kosztem większych nakładów obliczeniowych. Algorytm musimy dobrać świadomie do charakterystyki systemu, zamiast traktować go jako rozwiązanie uniwersalne.</p>
<h1 id="heading-podsumowanie">Podsumowanie</h1>
<p>Deterministyczne identyfikatory nie są rozwiązaniem uniwersalnym, ale w odpowiednich scenariuszach potrafią znacząco uprościć backend. W Cosmos DB pozwalają unikać kosztownych skanów, a w systemach rozproszonych wspierają idempotencję i prostsze integracje. Dzięki temu stają się jednym z tych detali architektonicznych, które zwracają się wielokrotnie w trakcie rozwoju systemu.</p>
<p>Dobrze zaprojektowane ID może być różnicą między tanim odczytem a kosztownym skanem całej bazy.</p>
<h2 id="heading-tabela-wyboru-sposobu-nadawania-kluczy-w-cosmosdb">Tabela wyboru sposobu nadawania kluczy w CosmosDB</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Podejście</td><td>Zalety</td><td>Wady</td></tr>
</thead>
<tbody>
<tr>
<td>Klucz techniczny (GUID, IDENTITY)</td><td>Łatwy do wygenerowania, stabilny w czasie, stała długość</td><td>Wymaga zapytań O(n), trudny do czytania</td></tr>
<tr>
<td>Klucz naturalny - prosty</td><td>O(1) w CosmosDB, wsparcie indempotencji</td><td>Problemy przy mutowalnych polach, zmienna długość kluczy, widoczne dane</td></tr>
<tr>
<td>Klucz naturalny - hash</td><td>O(1) w CosmosDB, stała długość, ukrycie danych</td><td>Trudny do czytania, koszt obliczeniowy</td></tr>
</tbody>
</table>
</div>]]></content:encoded></item><item><title><![CDATA[Double-checked locking w .NET – jak zatrzymać pędzące stado]]></title><description><![CDATA[Wprowadzenie
W systemach opartych na zdarzeniach oraz w aplikacjach wielowątkowych często pojawia się problem nagłego wzrostu obciążenia w momencie, gdy wiele wątków jednocześnie próbuje uzyskać dostęp do tego samego zasobu. Jeśli ten jest niedostępn...]]></description><link>https://beniaminlenarcik.pl/double-checked-locking-w-net-jak-zatrzymac-pedzace-stado</link><guid isPermaLink="true">https://beniaminlenarcik.pl/double-checked-locking-w-net-jak-zatrzymac-pedzace-stado</guid><category><![CDATA[double-checked-locking]]></category><category><![CDATA[pędzące-stado]]></category><category><![CDATA[thundering-herd]]></category><category><![CDATA[.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[patterns]]></category><category><![CDATA[pattern ]]></category><category><![CDATA[lock]]></category><category><![CDATA[eda]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[asynchronous]]></category><category><![CDATA[lazy loading]]></category><category><![CDATA[cache]]></category><category><![CDATA[spike ]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Wed, 17 Sep 2025 19:35:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758059660432/18178024-4eb4-42ec-9077-45d4fe9cd965.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-wprowadzenie">Wprowadzenie</h1>
<p>W systemach opartych na zdarzeniach oraz w aplikacjach wielowątkowych często pojawia się problem nagłego wzrostu obciążenia w momencie, gdy wiele wątków jednocześnie próbuje uzyskać dostęp do tego samego zasobu. Jeśli ten jest niedostępny, każdy proces może rozpocząć kosztowną operację jego pozyskania. Zjawisko to nazywamy problemem pędzącego stada (thundering herd problem). Wszystkie jednostki wykonawcze ruszają w tym samym kierunku, a system musi zmierzyć się ze skokowym wzrostem obciążenia.</p>
<p>Odpowiedzią na to wyzwanie jest zastosowanie double-checked locking. Technika pozwala uniknąć równoległego odświeżania tego samego zasobu i bezpiecznie udostępnia go wszystkim potrzebującym.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758134249400/7110ffdc-ffab-421a-a26c-b857d0d8a5c1.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-na-czym-polega-double-checked-locking">Na czym polega double-checked locking</h1>
<p>Wzorzec powstał w świecie wielowątkowego programowania jako optymalizacja dla klasycznego podejścia z blokadą. Proste użycie <code>lock</code> przy każdym dostępie do współdzielonego elementu jest kosztowne. Każdy wątek musi wtedy wejść do sekcji krytycznej, czyli fragmentu kodu otoczonego blokadą, do którego w danym momencie może wejść tylko jedna jednostka wykonawcza.</p>
<p>Double-checked locking pozwala ograniczyć ten narzut. Polega na podwójnym sprawdzaniu:</p>
<ol>
<li><p>Poza sekcją krytyczną – sprawdzamy, czy instancja już istnieje i jest gotowa do użycia.</p>
</li>
<li><p>Wewnątrz sekcji krytycznej – tuż po uzyskaniu blokady sprawdzamy, czy inny wątek nie wykonał tej pracy wcześniej i wynik nie jest już gotowy.</p>
</li>
</ol>
<p>Dzięki temu blokada używana jest wyłącznie w momencie faktycznej inicjalizacji, a nie przy każdym dostępie.</p>
<h1 id="heading-sterownik-iot-odczyt-kosztownego-czujnika">Sterownik IoT – odczyt kosztownego czujnika</h1>
<p>Wyobraźmy sobie urządzenie pomiarowe z wbudowanym czujnikiem jakości powietrza. Każdy odczyt z sensora jest kosztowny energetycznie i zajmuje kilka sekund. W tym samym procesie działa kilka modułów (np. monitor wentylacji, logger danych, moduł alarmowy), które co pewien czas potrzebują aktualnych wartości. Gdy wszystkie naraz zauważą, że poprzednie dane się przedawniły, jednocześnie spróbują uruchomić nowy, kosztowny pomiar. Zastosowanie mechanizmu double-checked locking rozwiązuje ten problem.</p>
<h2 id="heading-implementacja">Implementacja</h2>
<p>Najpierw sprawdzamy, czy trzymany w pamięci rekord <code>AirQualitySensorData</code> jeszcze się nie przedawnił. Jeśli odczyt jest “świeży” zwracamy wynik bez żadnych blokad. W innym wypadku wątki przechodzą przez semafor <code>await semaphore.WaitAsync()</code>, który wpuszcza tylko jeden z nich na raz do sekcji krytycznej. Jeśli na wejście czeka kilka wątków i nie wystąpił błąd, to pierwszy z nich przeprowadzi odczyt z czujnika. Dlatego tuż za przejściem próbujemy po raz drugi zwrócić rekord z cache. Jeśli odczyt wciąż jest nieprawidłowy, pobieramy nowy z urządzenia, aktualizujemy cache i zwracamy wynik. Na końcu, niezależnie od wyniku czy ewentualnego wyjątku, w bloku <code>finally</code> wywoływane jest <code>semaphore.Release()</code>, aby kolejne wątki mogły wejść do sekcji krytycznej. Dzięki temu rozwiązaniu unikamy wielokrotnych równoległych odczytów z sensora, a większość wywołań kończy się szybkim zwróceniem danych z pamięci.</p>
<pre><code class="lang-csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Gets current air quality data, performing expensive sensor read only if necessary.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Uses double-checked locking with SemaphoreSlim to prevent multiple concurrent expensive operations.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;AirQualitySensorData&gt; <span class="hljs-title">GetCurrentDataAsync</span>(<span class="hljs-params"></span>)</span>
{
    <span class="hljs-comment">// First check: Is cached data still valid? (no lock needed for read)</span>
    <span class="hljs-keyword">if</span> (cachedData?.IsValid(validityDuration) == <span class="hljs-literal">true</span>)
    {
        <span class="hljs-keyword">return</span> cachedData;
    }

    <span class="hljs-comment">// Acquire semaphore to ensure only one thread can proceed to expensive operation</span>
    <span class="hljs-keyword">await</span> semaphore.WaitAsync();
    <span class="hljs-keyword">try</span>
    {
        <span class="hljs-comment">// Second check: Re-verify data validity after acquiring semaphore</span>
        <span class="hljs-keyword">if</span> (cachedData?.IsValid(validityDuration) == <span class="hljs-literal">true</span>)
        {
            <span class="hljs-keyword">return</span> cachedData;
        }

        <span class="hljs-comment">// Perform the expensive sensor read operation and update cache</span>
        cachedData = <span class="hljs-keyword">await</span> airQualitySensor.ReadSensorAsync();

        <span class="hljs-comment">// Return updated value</span>
        <span class="hljs-keyword">return</span> cachedData;
    }
    <span class="hljs-keyword">finally</span>
    {
        <span class="hljs-comment">// Ensure semaphore is released even if an exception occurs</span>
        semaphore.Release();
    }
}
</code></pre>
<h1 id="heading-mechanizmy-wspierajace-w-net">Mechanizmy wspierające w .NET</h1>
<p>Nie zawsze musimy pisać kod rozwiązujący problem pędzącego stada od zera. Platforma .NET udostępnia wysokopoziomowe narzędzia z których możemy skorzystać w zależności od modelu życia obiektu.</p>
<h2 id="heading-jednorazowa-inicjalizacja-operacji-asynchronicznych-bez-odswiezania">Jednorazowa inicjalizacja operacji asynchronicznych (bez odświeżania)</h2>
<p>Wszyscy dobrze znamy mechanizm <code>Lazy&lt;T&gt;</code>, który służy do leniwej inicjalizacji obiektu. W przypadku operacji asynchronicznych możemy użyć <code>Lazy&lt;Task&lt;T&gt;&gt;</code>. Podejście sprawdzi się w sytuacji, gdy nie musimy odświeżać zasobu oraz nie potrzebujemy złożonego mechanizmu obsługi błędów lub ponawiania (retry) przy inicjalizacji.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// One-time asynchronous initialization using Lazy&lt;Task&lt;T&gt;&gt;</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> Lazy&lt;Task&lt;<span class="hljs-keyword">string</span>&gt;&gt; lazyConfig = 
    <span class="hljs-keyword">new</span> Lazy&lt;Task&lt;<span class="hljs-keyword">string</span>&gt;&gt;(LoadConfigurationAsync);

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Task&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">GetConfigAsync</span>(<span class="hljs-params"></span>)</span> =&gt; lazyConfig.Value;

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> Task&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">LoadConfigurationAsync</span>(<span class="hljs-params"></span>)</span>
{
    <span class="hljs-comment">// Simulate expensive operation</span>
    <span class="hljs-keyword">await</span> Task.Delay(<span class="hljs-number">1000</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Loaded configuration"</span>;
}
</code></pre>
<h2 id="heading-dane-odswiezanezanikajace-ttl-wiele-kluczy">Dane odświeżane/zanikające (TTL, wiele kluczy)</h2>
<p>Jeśli dane mają określony czas życia albo potrzebujemy przechowywać ich wiele (np. kolekcję konfiguracji, pomiary z różnych czujników), wygodnie skorzystać z <code>IMemoryCache</code>.</p>
<p>Model pozwala dla pobranych zasobów ustawić między innymi:</p>
<ul>
<li><p>AbsoluteExpiration – wygaszanie wpisów po ustalonym czasie,</p>
</li>
<li><p>SlidingExpiration – „przedłużanie życia” przy odczytach,</p>
</li>
<li><p>priorytety i czyszczenie – system usuwa mniej istotne wpisy przy presji pamięci,</p>
</li>
<li><p>wywołania zwrotne (callbacks) – możliwość reagowania, gdy element zostanie usunięty z cache.</p>
</li>
</ul>
<p>Jednak <code>IMemoryCache</code> nie eliminuje problemu pędzącego stada. Gdy wiele wątków jednocześnie zauważy brak lub wygaśnięcie wpisu, wszystkie mogą rozpocząć kosztowną operację jego odświeżenia.<br />Aby tego uniknąć, musimy dalej korzystać np. z mechanizmu double-checked locking.</p>
<p>Dodatkowo warto pamiętać, że <code>IMemoryCache</code> nie jest częścią podstawowej biblioteki .NET. Sam interfejs dostępny jest w <a target="_blank" href="https://www.nuget.org/packages/microsoft.extensions.caching.abstractions/">Microsoft.Extensions.Caching.Abstractions</a>. Domyślna implementacja znajduje się w <a target="_blank" href="https://www.nuget.org/packages/microsoft.extensions.caching.memory/">Microsoft.Extensions.Caching.Memory</a>, którą należy dodać do aplikacji, aby używać cache w runtime.</p>
<h3 id="heading-odczyt-z-imemorycache">Odczyt z <code>IMemoryCache</code></h3>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (cache.TryGetValue(<span class="hljs-string">"sensor-data"</span>, <span class="hljs-keyword">out</span> AirQualitySensorData data))
{
    <span class="hljs-keyword">return</span> data;
}
</code></pre>
<h3 id="heading-zapis-do-imemorycache-z-ttl">Zapis do <code>IMemoryCache</code> z TTL</h3>
<pre><code class="lang-csharp">cache.Set(<span class="hljs-string">"sensor-data"</span>, freshData, TimeSpan.FromSeconds(<span class="hljs-number">30</span>));
</code></pre>
<h1 id="heading-podsumowanie">Podsumowanie</h1>
<p>Problem pędzącego stada (thundering herd problem) może mieć różne oblicza w zależności od typu i dostępności zasobów. W platformie .NET mamy kilka sposobów, aby go powstrzymać.</p>
<p>Jeśli zasób musi być utworzony tylko raz i nie wymaga odświeżania, najprostsze rozwiązanie to <code>Lazy&lt;Task&lt;T&gt;&gt;</code>. Mechanizm jest prosty i bezpieczny, choć ogranicza możliwości obsługi błędów.</p>
<p>Dla danych, które tracą aktualność i wymagają okresowego odświeżania albo gdy operacja jest bardziej złożona, dobrze sprawdza się połączenie double-checked locking z <code>IMemoryCache</code>. Dzięki nim kosztowne wywołanie wykonuje się jeden raz w danym okresie, a wszystkie wątki korzystają z tego samego rezultatu.</p>
<p>Oba podejścia skutecznie ograniczają nagłe skoki obciążenia (spikes) i pozwalają bardziej efektywnie wykorzystać zasoby naszego systemu.</p>
<p>Ten artykuł jest częścią <a target="_blank" href="https://beniaminlenarcik.pl/series/uzyteczne-algorytmy-w-systemach-opartych-na-zdarzeniach">serii</a>. Przykładową implementację double-checked locking oraz inne użyteczne algorytmy przy przetwarzaniu zdarzeń znajdziesz w repozytorium: <a target="_blank" href="https://github.com/L3mur1/useful-async-algorithms">useful-async-algorithms</a></p>
]]></content:encoded></item><item><title><![CDATA[Jitter w .NET – jak rozkładać fale obciążeń]]></title><description><![CDATA[Wprowadzenie
Systemy oparte na zdarzeniach często muszą radzić sobie z sytuacją, w której wiele procesów uruchamia się w tym samym momencie. Dzieje się tak na przykład wtedy, gdy zadania są planowane na określone godziny albo gdy klienci jednocześnie...]]></description><link>https://beniaminlenarcik.pl/jitter-w-net-jak-rozkladac-fale-obciazen</link><guid isPermaLink="true">https://beniaminlenarcik.pl/jitter-w-net-jak-rozkladac-fale-obciazen</guid><category><![CDATA[jitter]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[events]]></category><category><![CDATA[asynchronous]]></category><category><![CDATA[async]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[C#]]></category><category><![CDATA[.NET]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[patterns]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Tue, 16 Sep 2025 06:45:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757972894347/e015e590-0745-4609-beff-1682c5b1fedd.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-wprowadzenie">Wprowadzenie</h1>
<p>Systemy oparte na zdarzeniach często muszą radzić sobie z sytuacją, w której wiele procesów uruchamia się w tym samym momencie. Dzieje się tak na przykład wtedy, gdy zadania są planowane na określone godziny albo gdy klienci jednocześnie ponawiają żądania w przypadku niedostępności usługi.</p>
<p>Jeśli wszystkie zdarzenia wypadają w tych samych chwilach, serwer zostaje zalany falą obciążenia, tworząc gwałtowne piki (spikes). To zjawisko określa się mianem thundering herd problem.</p>
<p>Rozwiązaniem jest jitter – algorytm wprowadzający niewielki, losowy czas oczekiwania przed przetwarzaniem zdarzenia. Dzięki temu nacisk na system rozkłada się w czasie, a fale obciążenia przestają być groźne.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757972925873/12b872f5-31e1-4bfc-ae53-8968c3d92f86.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-skad-pochodzi-jitter">Skąd pochodzi jitter</h1>
<p>Termin „jitter” pochodzi z telekomunikacji i oznacza zmienność opóźnienia sygnału. W transmisji danych – na przykład w rozmowie głosowej przez internet – informacje nie docierają w równych odstępach czasu. Jedna próbka dźwięku może przyjść po 20 ms, kolejna po 40 ms, a następna znowu szybciej. Dzieje się tak dlatego, że pakiety w sieci internetowej mogą iść różnymi trasami i napotykać odmienne opóźnienia. Takie zjawisko jest negatywne. Wprowadza dodatkową złożoność i pogarsza jakość transmisji.</p>
<p>W programowaniu jitter, wprowadzany w kontrolowany sposób jest czymś pozytywnym. Celowo implementujemy niewielkie rozchwianie czasów, by uniknąć sytuacji, w której wszystkie procesy wykonują się jednocześnie.</p>
<h1 id="heading-redukcja-skokow-obciazenia-w-systemie-monitorowania-zuzycia-energii-elektrycznej">Redukcja skoków obciążenia w systemie monitorowania zużycia energii elektrycznej</h1>
<p>Załóżmy, że tworzymy system monitorowania zużycia energii elektrycznej przez nasze urządzenia IoT. Nie mamy potrzeby dostarczania raportów na bieżąco. Zdecydowaliśmy, że każde urządzenie będzie wysyłać nam dane analityczne w nocy. Możemy założyć, że nasza firma odnosi sukcesy i liczba urządzeń u niezależnych klientów przekracza już 100 000. Jak rozłożyć przesyłanie danych do naszego systemu raportowania, tak by efektywnie zarządzić obciążeniem? Wprowadzenie jitter rozwiązuje ten problem.</p>
<h2 id="heading-implementacja">Implementacja</h2>
<p>Zamiast pozwalać wszystkim urządzeniom wysyłać dane dokładnie o północy, rozkładamy je na okno czasowe 00:00 – 01:00. Każde urządzenie dostaje indywidualne opóźnienie w tym przedziale.</p>
<h3 id="heading-fixed-jitter">Fixed Jitter</h3>
<p><strong>Idea:</strong> bazowy czas + losowa wartość z przedziału [0 - maxJitter]</p>
<pre><code class="lang-csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Fixed jitter that prevents server overload by spreading energy reports over time.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Each call waits for baseDelay plus a random amount up to maxJitter.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="baseDelay"&gt;</span>Base wait time (always applied)<span class="hljs-doctag">&lt;/param&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="maxJitter"&gt;</span>Maximum additional random delay<span class="hljs-doctag">&lt;/param&gt;</span></span>
<span class="hljs-function"><span class="hljs-keyword">public</span> class <span class="hljs-title">EnergyReportFixedJitter</span>(<span class="hljs-params">TimeSpan baseDelay, TimeSpan maxJitter</span>)</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TimeSpan baseDelay = baseDelay;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TimeSpan maxJitter = maxJitter;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> Random random = <span class="hljs-keyword">new</span>();

    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> Waits for baseDelay + random jitter (0 to maxJitter) before sending.</span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">SendWithJitterAsync</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> jitterMilliseconds = random.Next(<span class="hljs-number">0</span>, (<span class="hljs-keyword">int</span>)maxJitter.TotalMilliseconds);
        <span class="hljs-keyword">var</span> totalDelay = baseDelay.Add(TimeSpan.FromMilliseconds(jitterMilliseconds));

        <span class="hljs-keyword">await</span> Task.Delay(totalDelay);

        <span class="hljs-comment">// Sends report now</span>
    }
}
</code></pre>
<h3 id="heading-percentage-jitter">Percentage jitter</h3>
<p><strong>Idea:</strong> opóźnienie jest proporcją czasu bazowego, np. ±20%.</p>
<pre><code class="lang-csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Percentage jitter that prevents server overload by spreading energy reports over time.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Each call waits for baseDelay plus a random percentage of baseDelay.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="baseDelay"&gt;</span>Base wait time (always applied)<span class="hljs-doctag">&lt;/param&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="maxJitterPercentage"&gt;</span>Maximum random percentage of baseDelay to add (0-100)<span class="hljs-doctag">&lt;/param&gt;</span></span>
<span class="hljs-function"><span class="hljs-keyword">public</span> class <span class="hljs-title">EnergyReportPercentageJitter</span>(<span class="hljs-params">TimeSpan baseDelay, <span class="hljs-keyword">int</span> maxJitterPercentage</span>)</span>
{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TimeSpan baseDelay = baseDelay;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">int</span> maxJitterPercentage = maxJitterPercentage;
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> Random random = <span class="hljs-keyword">new</span>();

    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> Waits for baseDelay + random percentage jitter before sending.</span>
    <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">SendWithJitterAsync</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">var</span> jitterPercentage = random.Next(<span class="hljs-number">0</span>, maxJitterPercentage + <span class="hljs-number">1</span>);
        <span class="hljs-keyword">var</span> jitterMilliseconds = (<span class="hljs-keyword">int</span>)(baseDelay.TotalMilliseconds * jitterPercentage / <span class="hljs-number">100.0</span>);
        <span class="hljs-keyword">var</span> totalDelay = baseDelay.Add(TimeSpan.FromMilliseconds(jitterMilliseconds));

        <span class="hljs-keyword">await</span> Task.Delay(totalDelay);

        <span class="hljs-comment">// Sends report now</span>
    }
}
</code></pre>
<h1 id="heading-podsumowanie">Podsumowanie</h1>
<p>Jitter to prosta, ale bardzo skuteczna technika pozwalająca uniknąć przeciążenia aplikacji w momentach skumulowanego ruchu. Zamiast pozwalać, by wszystkie zdarzenia były procesowane jednocześnie, celowo wprowadzamy niewielką losowość w harmonogramie lub czasie ponawiania. Dzięki temu serwer nie doświadcza gwałtownych pików obciążenia. Nasz system działa efektywnie, stabilnie i jego zachowanie jest przewidywalne.</p>
<p>Ten artykuł jest częścią <a target="_blank" href="https://beniaminlenarcik.pl/series/uzyteczne-algorytmy-w-systemach-opartych-na-zdarzeniach">serii</a>. Przykładową implementację jitter oraz inne użyteczne algorytmy przy przetwarzaniu zdarzeń znajdziesz w repozytorium: <a target="_blank" href="https://github.com/L3mur1/useful-async-algorithms">useful-async-algorithms</a></p>
]]></content:encoded></item><item><title><![CDATA[Debounce w .NET – jak zatrzymać lawinę zdarzeń]]></title><description><![CDATA[Wprowadzenie
Czasem w systemie zaczyna pojawiać się zbyt dużo zdarzeń. Mogą być one powtarzalne, nadmiernie szczegółowe albo po prostu nieistotne w dużej liczbie. Taka lawina informacji przeciąża nasze rozwiązanie. Potrzebujemy  mechanizmu, który ogr...]]></description><link>https://beniaminlenarcik.pl/debounce-w-net-jak-zatrzymac-lawine-zdarzen</link><guid isPermaLink="true">https://beniaminlenarcik.pl/debounce-w-net-jak-zatrzymac-lawine-zdarzen</guid><category><![CDATA[debouncing]]></category><category><![CDATA[debounce]]></category><category><![CDATA[Debouncing and Throttling]]></category><category><![CDATA[event-driven-architecture]]></category><category><![CDATA[events]]></category><category><![CDATA[asynchronous]]></category><category><![CDATA[async]]></category><category><![CDATA[algorithms]]></category><category><![CDATA[.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[patterns]]></category><category><![CDATA[pattern ]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Tue, 09 Sep 2025 06:23:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757311578626/def3bc8d-84d8-41f1-a2f4-ea9bcf120e1f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-wprowadzenie">Wprowadzenie</h1>
<p>Czasem w systemie zaczyna pojawiać się zbyt dużo zdarzeń. Mogą być one powtarzalne, nadmiernie szczegółowe albo po prostu nieistotne w dużej liczbie. Taka lawina informacji przeciąża nasze rozwiązanie. Potrzebujemy  mechanizmu, który ograniczy częstość ich występowania. Jest nim debounce.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757972021750/4d5c977c-c75b-4920-9193-1700efddfa63.png" alt="Debounce redukuje wiele powtarzalnych zdarzeń do jednego sygnału" class="image--center mx-auto" /></p>
<h2 id="heading-skad-pochodzi-debounce">Skąd pochodzi debounce</h2>
<p>Termin „debounce” pochodzi z elektroniki. Mechaniczne przyciski po naciśnięciu potrafią kilkukrotnie „odbijać” styki, generując serię krótkich impulsów zamiast jednego sygnału. Aby zapobiec błędnemu odczytowi wielu kliknięć, stosuje się układ <em>debouncing</em>, który czeka, aż sygnał się ustabilizuje. Ta sama koncepcja została zaadaptowana w programowaniu zdarzeń.</p>
<h1 id="heading-redukcja-powtarzalnych-zdarzen-przy-monitorowaniu-zmian-w-plikach">Redukcja powtarzalnych zdarzeń przy monitorowaniu zmian w plikach</h1>
<p>Wyobraźmy sobie proces monitorowania zmian na dysku za pomocą FileSystemWatcher. Z jednej operacji potrafi pojawić się kilka różnych zdarzeń dotyczących tego samego zasobu. Sygnały mogą dotyczyć modyfikacji, zmiany rozmiaru, pojawienia się pliku itd. Wszystkie występują w bliskich odstępach czasowych. Reakcja na każde z nich osobno oznaczałaby kilkukrotne przeprocesowanie tych samych informacji. Zastosowanie mechanizmu debounce rozwiązuje ten problem.</p>
<h2 id="heading-implementacja">Implementacja</h2>
<p>Pełną wersję przykładu wraz z innymi algorytmami asynchronicznymi znajdziesz w repozytorium:</p>
<p><a target="_blank" href="https://github.com/L3mur1/useful-async-algorithms">https://github.com/L3mur1/useful-async-algorithms</a></p>
<p>Tworząc debounce określamy tzw. <em>debouncing window</em> – przedział czasu, w którym kolejne zdarzenia uznajemy za duplikaty.</p>
<pre><code class="lang-csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Events with the same path within this time span are ignored.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span>,</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TimeSpan debounceWindow;
</code></pre>
<p>Do podstawowej implementacji wystarczy pamiętać ostatni czas obsłużonego zdarzenia i porównać go z aktualnym. W przypadku plików musimy dodatkowo wziąć pod uwagę ścieżkę, ponieważ każde zdarzenie dotyczy innego zasobu. Dlatego używamy słownika <code>ConcurrentDictionary</code>, w którym trzymamy ostatni czas publikacji dla każdego zasobu osobno.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Stores the last event time for each file path to support debouncing.</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ConcurrentDictionary&lt;<span class="hljs-keyword">string</span>, DateTime&gt; lastEventTimes = <span class="hljs-keyword">new</span>();
</code></pre>
<p>Gdy pojawiają się kolejne sygnały, porównujemy ich czas wystąpienia z ostatnim zarejestrowanym dla danej ścieżki. Pomijamy zdarzenia, które wystąpiły wewnątrz deboucing window dla tego samego zasobu. Jeśli zdarzenie występuje poza oknem czasowym, uzupełniamy słownik i publikujemy.</p>
<pre><code class="lang-csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> Handles incoming events and applies debouncing based on the window and path.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnNext</span>(<span class="hljs-params">FileEvent fileEvent</span>)</span>
{
    <span class="hljs-keyword">if</span> (lastEventTimes.TryGetValue(fileEvent.Path, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> lastTime))
    {
        <span class="hljs-comment">// Check if the event is within the debounce window</span>
        <span class="hljs-keyword">if</span> (fileEvent.PublishTime - lastTime &lt; debounceWindow)
        {
            <span class="hljs-comment">// Ignore event within deboucing window</span>
            <span class="hljs-keyword">return</span>;
        }
    }

    <span class="hljs-comment">// Publish event and update last event time for path</span>
    lastEventTimes[fileEvent.Path] = fileEvent.PublishTime;
    subject.OnNext(fileEvent);
}
</code></pre>
<p>Warto pamiętać, że w przypadku monitorowania wielu ścieżek, np. całych folderów, słownik może rosnąć w nieskończoność i niepotrzebnie zabierać pamięć systemu. Warto wprowadzić mechanizm okresowego czyszczenia zasobów.</p>
<pre><code class="lang-csharp"><span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> lastEventTimes dictionary should be periodically cleaned up</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> to avoid memory leaks from paths that are no longer active.</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> this is example clean up</span>
<span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CleanUp</span>(<span class="hljs-params"><span class="hljs-keyword">long</span> obj</span>)</span>
{
    <span class="hljs-keyword">var</span> threshold = DateTime.UtcNow - debounceWindow;
    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> kvp <span class="hljs-keyword">in</span> lastEventTimes)
    {
        <span class="hljs-comment">// Remove entries older than the threshold</span>
        <span class="hljs-keyword">if</span> (kvp.Value &lt; threshold)
        {
            lastEventTimes.TryRemove(kvp.Key, <span class="hljs-keyword">out</span> _);
        }
    }
}
</code></pre>
<p>Dzięki zastosowaniu debounce zamiast lawiny zdarzeń dostajemy tylko jedno – reprezentatywne dla konkretnej ścieżki – w danym oknie czasowym.</p>
<h1 id="heading-podsumowanie">Podsumowanie</h1>
<p>Debounce to algorytm redukujący liczbę przetwarzanych zdarzeń w systemie. Jego istotą jest powiązanie różnych sygnałów w krótkich odstępach czasu. Ma to szczególne znaczenie w przetwarzaniu zdarzeń w architekturach event-driven, które z natury są asynchroniczne i nie dają gwarancji spójności. Stosując ten wzorzec tworzymy systemy mniej hałaśliwe i bardziej oszczędne w zasobach.</p>
<p>Ten artykuł jest częścią <a target="_blank" href="https://beniaminlenarcik.pl/series/uzyteczne-algorytmy-w-systemach-opartych-na-zdarzeniach">serii</a>. Przykładową implementację debounce oraz inne użyteczne algorytmy przy przetwarzaniu zdarzeń znajdziesz w repozytorium: <a target="_blank" href="https://github.com/L3mur1/useful-async-algorithms">useful-async-algorithms</a></p>
]]></content:encoded></item><item><title><![CDATA[System.IO - abstrakcja czy iluzja?]]></title><description><![CDATA[Wprowadzenie
Pakiet System.IO dla .NET znacznie uproszcza zarządzanie plikami. Przykładowo, pozwala kopiować je jedną linijką kodu. Działa świetnie na lokalnym dysku. Niestety, gdy plik leży na zdalnym serwerze czy network share – proste API System.I...]]></description><link>https://beniaminlenarcik.pl/systemio-abstrakcja-czy-iluzja</link><guid isPermaLink="true">https://beniaminlenarcik.pl/systemio-abstrakcja-czy-iluzja</guid><category><![CDATA[system.io]]></category><category><![CDATA[abstrakcja]]></category><category><![CDATA[iluzja]]></category><category><![CDATA[abstraction]]></category><category><![CDATA[Illusion]]></category><category><![CDATA[.NET]]></category><category><![CDATA[interface]]></category><category><![CDATA[APIs]]></category><category><![CDATA[Design]]></category><category><![CDATA[Complexity]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Sat, 06 Sep 2025 11:33:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757445101310/af89f96a-ca57-40c8-ae66-16721f92deb6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-wprowadzenie">Wprowadzenie</h1>
<p>Pakiet <code>System.IO</code> dla .NET znacznie uproszcza zarządzanie plikami. Przykładowo, pozwala kopiować je jedną linijką kodu. Działa świetnie na lokalnym dysku. Niestety, gdy plik leży na zdalnym serwerze czy network share – proste API <code>System.IO</code> przestaje być tylko wygodną abstrakcją i zaczyna przypominać iluzję. Ukrywa bowiem rzeczywiste wyzwania integracji między systemami.</p>
<h1 id="heading-zlozonosc-complexity-schowana-w-systemio">Złożoność (complexity) schowana w <code>System.IO</code></h1>
<p><code>System.IO</code> to prawdziwy triumf abstrakcji. Jeden prosty interfejs do zarządzania plikami:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">string</span> content = File.ReadAllText(<span class="hljs-string">"dokument.txt"</span>);
File.WriteAllText(<span class="hljs-string">"kopia.txt"</span>, content);
</code></pre>
<p>Pod fasadą kryje się cała masa skomplikowanych operacji których nie chcemy obsługiwać tworząc nowe rozwiązania dla biznesu. Między innymi należą do nich:</p>
<ul>
<li><p>automatyczne wykrywanie kodowania znaków,</p>
</li>
<li><p>buforowanie danych w pamięci,</p>
</li>
<li><p>zarządzanie uchwytami do plików (handles),</p>
</li>
<li><p>synchronizacja dostępu między procesami.</p>
</li>
</ul>
<p>Wszystko działa świetnie w lokalnym środowisku Windows. Iluzja pojawia się, gdy używamy <code>System.IO</code> z zasobami przechowywanymi w sieci. Jest to nowa złożoność, której nie da się już zaprogramować na jeden sposób za prostą fasadą. Obsługa jej wymaga dodatkowej, decyzyjnej logiki od programisty.</p>
<h1 id="heading-iluzja-systemio-w-sieci">Iluzja <code>System.IO</code> w sieci</h1>
<p>Proste API <code>System.IO</code> sprawia wrażenie, że operacje na plikach są zawsze przewidywalne. Jednak w przypadku zasobów sieciowych to założenie z abstrakcji szybko zamienia się w iluzję.</p>
<h3 id="heading-podstawowe-operacje-na-pliku">Podstawowe operacje na pliku</h3>
<p>Na lokalnym dysku zapis czy odczyt zwykle są szybkie i niezawodne:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Zapis pliku – zwykle szybka operacja lokalnie</span>
File.WriteAllText(<span class="hljs-string">"plik.txt"</span>, content);
</code></pre>
<p>Na udziale sieciowym ta sama instrukcja może trwać sekundy lub nawet minuty. Pod spodem nie ma już zapisu na lokalny dysk tylko faktyczna transmisja danych przez protokół – SMB, NFS, FTP czy inny mechanizm udostępniania plików. To one decydują o tym, czy zapis będzie atomowy, czy zerwie się w połowie, jak obsłużone będą blokady i w jaki sposób raportowane są błędy. Fasada ukrywa całą tę złożoność, dając iluzję prostoty.</p>
<h2 id="heading-zbyt-dlugi-czas-operacji">Zbyt długi czas operacji</h2>
<p>W sieci, nawet prosta operacja może zająć więcej czasu. By zabezpieczyć system przed takimi przypadkami warto użyć metod asynchronicznych. W nich można przekazać CancellationToken i w ten sposób kontrolować timeout:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Zapis pliku metodą asynchroniczną - obsługa części problemów przez przekazanie cancellationToken</span>
cts.CancelAfter(<span class="hljs-number">3500</span>);
WriteAllTextAsync(<span class="hljs-string">"plik.text"</span>, content, cts.CancellationToken);
</code></pre>
<p>Użycie async nie rozwiązuje jednak problemu zanikającego połączenia czy potrzeby ponowienia operacji.</p>
<h2 id="heading-ponowienie-i-zapis-posredni">Ponowienie i zapis pośredni</h2>
<p>Retry w przypadku niestabilnego połączenia to standard w komunikacji HTTP – w .NET zapewnia to np. biblioteka Polly. W przypadku integracji file transfer mechanizmy ponowienia pozostają jednak w gestii programisty.</p>
<p>Przykładowa implementacja prostego retry:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Przykładowa implementacja ponawiania zapisu</span>
<span class="hljs-keyword">int</span> retries = <span class="hljs-number">3</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; retries; i++)
{
    <span class="hljs-keyword">try</span>
    {
        File.WriteAllText(<span class="hljs-string">@"\\server\plik.txt"</span>, content);
        <span class="hljs-keyword">break</span>;
    }
    <span class="hljs-keyword">catch</span> (IOException) <span class="hljs-keyword">when</span> (i &lt; retries - <span class="hljs-number">1</span>)
    {
        Thread.Sleep(<span class="hljs-number">1000</span>); <span class="hljs-comment">// prosty backoff</span>
    }
}
</code></pre>
<p>Drugim częstym wyzwaniem w protokołach SMB, NFS czy FTP jest brak jednoznacznej informacji o kompletności pliku. Jeśli drugi system reaguje na pojawienie się nowego zasobu na dysku sieciowym, często robi to jeszcze przed pełnym zakończeniem zapisu i zwolnieniem pliku. Prowadzi to do wyjątków przy próbie odczytu.</p>
<p>Typowym rozwiązaniem jest tworzenie pliku “.done” po zakończeniu operacji lub zapis do pliku tymczasowego “.tmp“, a dopiero na koniec zmiana nazwy na docelową:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Zapis do pliku tymczasowego</span>
File.WriteAllText(<span class="hljs-string">"plik.txt.tmp"</span>, content);

<span class="hljs-comment">// Dopiero po pełnym sukcesie – zmiana nazwy na docelową</span>
File.Move(<span class="hljs-string">"plik.txt.tmp"</span>, <span class="hljs-string">"plik.txt"</span>);
</code></pre>
<p>Opisane sytuacje pokazują, że przy pracy z plikami w sieci to programista musi świadomie przejąć kontrolę nad logiką i rozumieć złożoność ukrytą pod fasadą <code>System.IO</code>.</p>
<h1 id="heading-podsumowanie">Podsumowanie</h1>
<p><code>System.IO</code> jest świetnym przykładem siły fasady, dopóki działamy w prostym, lokalnym środowisku. Wystarczy jednak przenieść pliki do sieci, by ta wygodna abstrakcja zaczęła przypominać iluzję. Problem nie leży tak naprawdę w samej bibliotece .NET, lecz w protokołach działających pod spodem – SMB, NFS czy FTP – które próbują symulować zachowanie lokalnego dysku, choć w rzeczywistości wykonują zawodny i kosztowny transfer sieciowy. To one decydują, czy zapis potrwa dwie sekundy czy dwie minuty, czy operacja zakończy się atomowo, czy w połowie, i jak zostaną obsłużone blokady.</p>
<p>W takich scenariuszach deweloper musi rozumieć ukrytą złożoność: świadomie nadpisać wybory podjęte pod fasadą albo opakować kod dodatkowymi mechanizmami (retry, zapis pośredni, kontrola timeoutów). Przykład <code>System.IO</code> pokazuje, że żadna abstrakcja nie potrafi całkowicie ukryć działania niższych warstw. Projektując własne interfejsy warto dążyć do maksymalnego uproszczenia, ale nie kosztem decyzji, które powinny pozostać w gestii programisty. Tam, gdzie próbujemy je ukryć, zamiast mocnej abstrakcji tworzymy jedynie iluzję.</p>
]]></content:encoded></item><item><title><![CDATA[Azure Functions – na dzisiaj ślepa uliczka dla API]]></title><description><![CDATA[Azure Functions – obietnica serverless, rzeczywistość dla API
Azure Functions obiecywały wiele: skalowalność, niski koszt, zero zarządzania infrastrukturą. W teorii to idealne miejsce na wystawienie prostego API, zwłaszcza przy nieregularnym ruchu. W...]]></description><link>https://beniaminlenarcik.pl/azure-functions-na-dzisiaj-slepa-uliczka-dla-api</link><guid isPermaLink="true">https://beniaminlenarcik.pl/azure-functions-na-dzisiaj-slepa-uliczka-dla-api</guid><category><![CDATA[asp.net core]]></category><category><![CDATA[ASP.NET]]></category><category><![CDATA[Azure]]></category><category><![CDATA[Azure Functions]]></category><category><![CDATA[APIs]]></category><category><![CDATA[REST API]]></category><category><![CDATA[#JokesPortal]]></category><category><![CDATA[.NET]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Thu, 31 Jul 2025 11:08:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753960007554/632d26c9-755a-4317-b6fc-4f717e542337.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-azure-functions-obietnica-serverless-rzeczywistosc-dla-api">Azure Functions – obietnica serverless, rzeczywistość dla API</h1>
<p>Azure Functions obiecywały wiele: skalowalność, niski koszt, zero zarządzania infrastrukturą. W teorii to idealne miejsce na wystawienie prostego API, zwłaszcza przy nieregularnym ruchu. W praktyce – funkcje nie są kompletnym frameworkiem webowym do budowy interfejsów HTTP. Tworzenie API w tym modelu często kończy się rozczarowaniem. Szczególnie teraz, gdy Microsoft porzuca stary model (in-process), a nowy (out-of-process) wprowadza poważne ograniczenia.</p>
<h1 id="heading-dwa-modele-in-process-vs-out-of-process">Dwa modele: in-process vs out-of-process</h1>
<p>Usługa Azure Functions zadebiutowała na platformie Azure w 2016 roku jako rozwiązanie serverless, które pozwala uruchamiać małe, niezależne funkcje reagujące na zdarzenia – takie jak żądania HTTP, wiadomości z kolejki czy zdarzenia czasowe – bez potrzeby zarządzania infrastrukturą. Początkowo funkcje działały wyłącznie w modelu in-process, czyli były uruchamiane w tym samym procesie, co host Azure Functions. Pozwalało to na korzystanie z naturalnego dla programistów zestawu narzędzi ASP.NET przy budowie API – takiego samego, jak w aplikacjach OnPremises, kontenerach Docker czy Azure App Service.</p>
<p>Z czasem jednak ten model zaczął ujawniać swoje ograniczenia. Współdzielenie procesu z hostem oznaczało brak izolacji środowisk, trudności w aktualizacji wersji .NET, konflikty zależności oraz ograniczoną możliwość niezależnego rozwoju i testowania. Problemem była też niższa niezawodność i większe ryzyko błędów przy uruchamianiu bardziej złożonych projektów.</p>
<p>Aby zaadresować te wyzwania, Microsoft wprowadził w 2020 roku model out-of-process (znany również jako isolated worker). Funkcje działają tu w osobnym procesie .NET, który komunikuje się z hostem za pomocą RPC. Taka separacja pozwala używać dowolnych wersji środowiska .NET niezależnie od platformy Azure Functions i unikać konfliktów wersji.</p>
<p>Model isolated eliminuje wiele ograniczeń technicznych modelu in-process, ale jednocześnie rezygnuje z kilku kluczowych cech. Microsoft zdecydował, że to właśnie model out-of-process będzie jedynym wspieranym w przyszłości. <strong>Wsparcie dla Azure Functions in-process zakończy się w listopadzie 2026 roku.</strong></p>
<h1 id="heading-problemy-modelu-out-of-process-w-budowie-api">Problemy modelu out-of-process w budowie API</h1>
<p>Model out-of-process rozwiązuje pewne problemy techniczne starszego podejścia, ale w kontekście budowy API wprowadza wiele ograniczeń. Trudno je zaakceptować przy pracy nad rzeczywistymi usługami HTTP. Poniżej przedstawiam najważniejsze z nich:</p>
<h2 id="heading-brak-wsparcia-dla-aspnet">Brak wsparcia dla ASP.NET</h2>
<p>Nie można użyć UseMiddleware(), nie ma dostępu do IApplicationBuilder. Oznacza to, że nie zbuduje się automatycznie pełnego pipeline’u z autoryzacją, walidacją modeli, filtrowaniem błędów. Wszystko trzeba implementować inaczej i bardziej ręcznie. W praktyce oznacza to <strong>więcej kodu do utrzymania</strong> oraz <strong>utratę</strong> możliwości szybkiego zmianu sposobu hostowania API - gdy zdecydujesz się budowac w Isolated Model, <strong>migracja to innego środowiska będzie kosztowna</strong>.</p>
<h2 id="heading-brak-mozliwosci-uzycia-najlepszych-narzedzi-do-generowania-dokumentacji-api">Brak możliwości użycia najlepszych narzędzi do generowania dokumentacji API</h2>
<p>W Azure Functions Isolated model, nie da się po prostu dodać Swashbuckle/Swagger do generowania dokumentacji jak w Web API. Potrzebna jest osobna biblioteka -<a target="_blank" href="https://www.nuget.org/packages/Microsoft.Azure.Functions.Worker.Extensions.OpenApi">Microsoft.Azure.Functions.Worker.Extensions.OpenAPI</a>. Niestety na dzisiaj ma ona ograniczenia. Nie wspiera wielu cech ASP.NET, na przykład generowania dokumentacji dla klas powiązanych dziedziczeniem należących do kontraktu. Dodatkowo konfiguracja biblioteki jest mniej intuicyjna, a dokumentacja bywa niejasna. <strong>DX (Developer Experience) wyraźnie się pogarsza.</strong></p>
<h2 id="heading-cold-start-i-niestabilna-wydajnosc">Cold start i niestabilna wydajność</h2>
<p>Model out-of-process wymaga uruchomienia osobnego procesu. W planie konsumpcyjnym może to oznaczać <strong>kilkadziesiąt sekund czekania na odpowiedź po dłuższej przerwie</strong>. Co gorsza, każdy endpoint HTTP traktowany jest niezależnie – jeśli Twoje API składa się z wielu funkcji, to <strong>każda z nich ma własny cold start</strong>. W modelu in-process wystarczyło rozgrzać jeden endpoint (np. /health), by uruchomić cały proces hosta i tym samym „wybudzić” wszystkie punkty dostępowe na raz. W modelu isolated to już nie działa. Cold starty są bardziej dotkliwe i trudniejsze do obejścia. W mojej ocenie to <strong>show stopper dla większości przypadków tworzenia publicznego AP</strong>I.</p>
<h1 id="heading-model-in-process-umiera-ale-byl-bardziej-wygodny">Model in-process – umiera, ale był bardziej wygodny</h1>
<p>Mimo swoich ograniczeń, model in-process był po prostu praktyczny dla małych API. Działał jak okrojony ASP.NET Web API – wspierał kontrolery, middleware, znane mechanizmy DI, a wiele gotowych narzędzi działało od ręki. Teraz Microsoft oficjalnie każe go porzucić. <strong>Od .NET 8 wspierany jest wyłącznie isolated worker</strong>. Jeśli korzystałeś wcześniej z in-process, będziesz musiał zaplanować migrację. Jeśli dopiero zaczynasz – warto rozważyć inne podejście do hostowania API, na przykład Azure App Service.</p>
<h1 id="heading-flex-consumption-odpowiedz-na-problem-cold-startow-w-azure-functions">Flex Consumption – odpowiedź na problem cold startów w Azure Functions</h1>
<p>Flex Consumption Plan pojawił się w Azure Functions w 2024 roku jako odpowiedź na problem cold startów. Ten model łączy zalety serverless podstawowego planu Consumption z utrzymywaniem funkcji w stanie „ciepłym” podobnie jak w planie Premium. Dzięki temu w API o nieregularnym ruchu pierwsze żądania trafiają do w pełni gotowych instancji, a ryzyko cold startu jest minimalne. Przy skalowaniu Azure korzysta z puli wstępnie przygotowanych instancji, dzięki czemu kolejne instancje uruchamiają się znacznie szybciej niż w klasycznym Consumption.</p>
<p>Niestety korzystanie z opcji Always Ready Instances oznacza stałe koszty – tu nie ma już darmowego miliona requestów miesięcznie, a płaci się cały czas, podobnie jak w App Service. Dodatkowo usługa nie eliminuje pozostałych ograniczeń modelu isolated.</p>
<h1 id="heading-azure-app-service-stary-dobry-kon-roboczy">Azure App Service – stary dobry koń roboczy</h1>
<p>Jeśli chcesz wystawić produkcyjne REST API, nawet małe, to Azure App Service z Minimal API lub Web API moim zdaniem będzie lepszym wyborem.</p>
<p>Dostajesz:</p>
<ul>
<li><p>pełne wsparcie powszechnie znanego przez deweloperów środowiska ASP.NET,</p>
</li>
<li><p>działające Swashbuckle w 3 linijki,</p>
</li>
<li><p>middleware, DI, filtry, walidację modeli,</p>
</li>
<li><p>przewidywalne czasy odpowiedzi i brak cold startów,</p>
</li>
<li><p>bardzo dobre lokalne testowanie – zwłaszcza jeśli używasz kontenerów</p>
</li>
<li><p>możliwość prostrzej migracji rozwiązania w przyszłości na inne platformy</p>
</li>
</ul>
<p>Azure App Service daje elastyczność. Możesz uruchomić API jako zwykłą aplikację lub jako kontener – zarówno na Windowsie, jak i Linuksie.</p>
<p>Choć nie ma planu płatności „pay-as-you-go”, to już dziś można mieć stale działające API produkcyjne na Linuksie za ok. 45 zł miesięcznie.</p>
<p>Jeśli więc chcesz hostować małe API bez obaw o wydajność i przewidywalność działania – moim zdaniem <strong>Azure App Service wygrywa jakością i ergonomią z Azure Functions Isolated Model</strong>.</p>
<h2 id="heading-jak-uzywam-azure-app-service">Jak używam Azure App Service</h2>
<p>Sam wykorzystuję Azure App Service do hostowania mojego pet projektu - aplikacji z żartami <a target="_blank" href="https://www.jokesportal.com/pl/">Jokes Portal</a>. Jest to aplikajca mobilna, która wykorzystuje Azure App Service. Działa 24/7, obsługuje realnych użytkowników i potrzebuje stabilnego API bez cold startów. Usługa sprawdza się tu znakomicie.</p>
<h1 id="heading-podsumowanie-nie-kazde-rozwiazanie-to-api">Podsumowanie: Nie każde rozwiązanie to API</h1>
<p>W tym artykule skupiłem się wyłącznie na przypadku budowy REST API. Nie twierdzę, że Azure Functions są złe – wręcz przeciwnie. Model isolated to świetne narzędzie do budowy systemów event driven w chmurze Azure. Funkcje wspierają integracje z niemal wszystkimi usługami Azure out-of-the-box i doskonale sprawdzają się w scenariuszach asynchronicznych oraz transakcjach rozproszonych (Durable Functions).</p>
<p>Ze względu na opisane wyżej ograniczenia uważam, że <strong>Azure Functions w modelu isolated nie nadają się dziś do budowy synchronicznych REST API</strong>. W takich przypadkach warto postawić na <strong>Azure App Service</strong>. To może nie jest modne, ale po prostu działa.</p>
]]></content:encoded></item><item><title><![CDATA[MCP (Model Context Protocol) — jak dać agentom AI realną możliwość działania w środowisku .NET]]></title><description><![CDATA[W ostatnich miesiącach coraz częściej mówi się o agentach AI — systemach, które potrafią nie tylko analizować dane, ale też podejmować konkretne działania w zależności od kontekstu. Mogą wykonywać polecenia zarówno z użyciem publicznych API, jak i w ...]]></description><link>https://beniaminlenarcik.pl/mcp-jak-dac-agentom-ai-realna-mozliwosc-dzialania-w-srodowisku-net</link><guid isPermaLink="true">https://beniaminlenarcik.pl/mcp-jak-dac-agentom-ai-realna-mozliwosc-dzialania-w-srodowisku-net</guid><category><![CDATA[mcp]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[github copilot]]></category><category><![CDATA[.NET]]></category><category><![CDATA[Model Context Protocol]]></category><category><![CDATA[Model Context Protocol (MCP)]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Wed, 04 Jun 2025 19:32:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749065511841/2ec0e7a8-1e4c-43ae-9878-e63dbe5a4404.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>W ostatnich miesiącach coraz częściej mówi się o agentach AI — systemach, które potrafią nie tylko analizować dane, ale też podejmować konkretne działania w zależności od kontekstu. Mogą wykonywać polecenia zarówno z użyciem publicznych API, jak i w zamkniętych, firmowych systemach — np. na komputerze użytkownika lub serwerze on-prem.</p>
<p>Aby umożliwić im faktyczne działanie, pojawia się MCP (Model Context Protocol) — otwarty standard rozwijany przez Anthropic, który ułatwia połączenie agenta AI z otoczeniem technologicznym, w którym funkcjonuje. W dalszej części pokażę, czym jest to rozwiązanie, jak wspiera agentów AI i jak wygląda jego implementacja w .NET.</p>
<h1 id="heading-jaki-problem-rozwiazuje-mcp-model-context-protocol">Jaki problem rozwiązuje MCP (Model Context Protocol)?</h1>
<p>Większość współczesnych integracji AI opiera się na dostępie do publicznych interfejsów — otwartych na świat API firm trzecich (takich jak Google Drive, Dropbox, Microsoft Teams), jak również naszych własnych aplikacji, które wystawiliśmy przez HTTP. Do tego dochodzą interfejsy udostępniane przez dostawców chmurowych, które dają dostęp do zasobów takich jak pliki, funkcje, kolejki czy bazy danych. Agenci AI świetnie radzą sobie z takim środowiskiem — o ile wszystko jest wystawione i dostępne z zewnątrz.</p>
<p>Choć w wielu przypadkach to podejście jest wystarczające, w środowiskach enterprise — ze względu na wysokie wymagania bezpieczeństwa — to zdecydowanie za mało. Nie możemy pozwolić, by agent sztucznej inteligencji miał bezpośredni dostęp do zasobów wewnętrznych przez publiczną sieć czy zewnętrzne usługi. Potrzebujemy modelu, w którym wykonanie polecenia odbywa się wewnątrz kontrolowanej infrastruktury, bez narażania wrażliwych zasobów firmowych.</p>
<p>Właśnie tu pojawia się Model Context Protocol (MCP) — otwarty standard, który standaryzuje sposób, w jaki modele językowe (LLM) mogą bezpiecznie korzystać z danych i narzędzi znajdujących się w systemach wewnętrznych. Model jest już na tyle popularny, że wspierają go najważniejsze rozwiązania agentyczne, takie jak GitHub Copilot czy Claude.</p>
<h1 id="heading-jak-dziala-mcp-model-context-protocol">Jak działa MCP (Model Context Protocol)?</h1>
<p>MCP pozwala modelowi językowemu odwoływać się do narzędzi uruchomionych poza jego środowiskiem — np. w sieci firmowej, systemie operacyjnym użytkownika czy w prywatnej infrastrukturze.</p>
<p>Całość opiera się na trzech komponentach:</p>
<ul>
<li><p><strong>MCP Host</strong> — to środowisko, w którym działa agent AI (np. Claude, GitHub Copilot). Host analizuje dostępne narzędzia (tools), które oferuje MCP Server, wybiera te dostępne w danym kontekście i decyduje, które komendy model może uruchamiać. Host potrafi też reagować na zdarzenia pochodzące z MCP Servera — protokół wspiera komunikację dwukierunkową.</p>
</li>
<li><p><strong>MCP Client</strong> — to komponent działający blisko modelu językowego (np. jako biblioteka w tym samym procesie). Obsługuje techniczne szczegóły protokołu — wysyła żądania do MCP Servera, odbiera odpowiedzi, monitoruje zdarzenia.</p>
</li>
<li><p><strong>MCP Server</strong> — to lokalna aplikacja, która działa w zaufanym środowisku (np. komputer użytkownika, serwer on-prem) i wykonuje komendy przekazane przez agenta. To właśnie tutaj odbywa się właściwe działanie — uruchamianie procesów, restart usług, czytanie plików, itd. Serwer implementuje również zabezpieczenia: ograniczenia dostępu, audyt, walidację wejścia.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749063587907/04bab653-50c2-4095-b0a8-3bcb9ceedc02.png" alt class="image--center mx-auto" /></p>
<p>  <strong>Schemat działania MCP:</strong> model językowy (LLM) komunikuje się z agentem (MCP Host), który deleguje polecenia do lokalnych serwerów (MCP Server) działających w zaufanym środowisku. Taka architektura umożliwia bezpieczne wykonywanie działań w systemach on-prem, bez konieczności udostępniania infrastruktury na zewnątrz.</p>
</li>
</ul>
<h1 id="heading-mcp-model-context-protocol-w-srodowisku-net">MCP (Model Context Protocol) w środowisku .NET</h1>
<p>W środowisku .NET możemy już dziś zacząć wdrażać MCP dzięki paczce NuGet <code>ModelContextProtocol</code> (link) stworzonej przez zespół Microsoftu.</p>
<p>Dodatkowo, na oficjalnym blogu .NET znajdziesz kompletny przykład implementacji MCP Servera w C#, z kodem źródłowym i omówieniem. Paczka jest obecnie w wersji prerelease, ale już dziś umożliwia budowanie agentów działających w wewnętrznym środowisku, z pełną kontrolą nad ich możliwościami.</p>
<h1 id="heading-rozwiazanie-devops-do-zarzadzania-serwerami-onprem-w-oparciu-o-github-copilot-i-mcp">Rozwiązanie DevOps do zarządzania serwerami OnPrem w oparciu o GitHub Copilot i MCP</h1>
<p>W wielu firmach operacje DevOps w środowiskach on-prem to codzienność. Często wymagają one ręcznego logowania się na serwery, analizy logów, restartowania usług czy aktualizacji konfiguracji.</p>
<p>Dzięki połączeniu GitHub Copilota i MCP możemy zbudować agenta AI, który:</p>
<ul>
<li><p>działa na lokalnej infrastrukturze,</p>
</li>
<li><p>rozumie język naturalny,</p>
</li>
<li><p>ma dostęp do MCP Serwera,</p>
</li>
<li><p>wykonuje polecenia (np. restart usługi, analiza logów),</p>
</li>
<li><p>reaguje na zdarzenia (np. awaria, brak przestrzeni dyskowej),</p>
</li>
<li><p>nie wymaga otwierania infrastruktury na zewnątrz.</p>
</li>
</ul>
<p>Co ważne — taki agent może działać samodzielnie, wykonywać skrypty, testować scenariusze A/B czy raportować wyniki testów.</p>
<p>Przykładem może być wykorzystanie agenta przez inżyniera QA:</p>
<ul>
<li><p>„Ustaw feature toggle w serwisie A i zresetuj usługę na środowisku X”</p>
</li>
<li><p>“Załóż konto klienta na serwerze A i B”</p>
</li>
<li><p>„Sprawdź czy klient jest widoczny w bazie danych na obu środowiskach”</p>
</li>
</ul>
<h2 id="heading-przykladowy-mcp-server-devops-w-net">Przykładowy MCP Server DevOps w .NET</h2>
<p>Przygotowałem prosty MCP Server w .NET, który działa na Windowsie i pozwala:</p>
<ul>
<li><p>listować usługi systemowe,</p>
</li>
<li><p>zatrzymywać i uruchamiać wybrane usługi.</p>
</li>
</ul>
<p>Repozytorium znajdziesz tutaj:</p>
<p><a target="_blank" href="https://github.com/L3mur1/MCPDevOps">https://github.com/L3mur1/MCPDevOps</a></p>
<p>A informacje jak połączyc VS Code i Github copilot z MCP tutaj:</p>
<p><a target="_blank" href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers">https://code.visualstudio.com/docs/copilot/chat/mcp-servers</a></p>
<p>Poniżej zdjęcia z rzeczywistego scenariusza, który wykonałem testowo z użyciem agenta GitHub Copilot połączonego z lokalnym MCP Serwer:</p>
<p>Na rozgrzewkę poprosiłem o listę wszystkich usług:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749064608657/7e16ac15-5ae1-4cb3-b879-dc0e73faa1a3.png" alt class="image--center mx-auto" /></p>
<p>Zapytałem agenta, którą usługę można zatrzymać testowo:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749064701009/65369739-a8fd-45c2-83c1-681966b3f7fd.png" alt class="image--center mx-auto" /></p>
<p>Zaskoczyło mnie, jak sprawnie agent podszedł do zadania. Sprawdził status kilku usług, oceniając czy są bezpieczne do zatrzymania.</p>
<p>Wyłączyłem usługę, przy okazji zwróćcie uwagę na wymaganą weryfikację przy pierwszym uruchomieniu komendy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749064944561/1530ba55-0fd2-4223-ba4f-83cc9c281bea.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749065018055/214a293c-3a27-4526-bf83-627dd250573a.png" alt class="image--center mx-auto" /></p>
<p>Następnie poleciłem ponowne uruchomienie:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749065088118/314f3b45-3de7-4c6b-b080-9b2e346046b0.png" alt class="image--center mx-auto" /></p>
<p>Nawet tak prosty MCP serwer daje możliwości z których możemy korzystać w codziennej pracy, np. do zatrzymania usług blokujących kompilacje aplikacji, czy otwarte pliki.</p>
<h1 id="heading-wnioski">Wnioski</h1>
<p>Agenci AI nie muszą ograniczać się do świata przeglądarki czy zewnętrznych API. Dzięki protokołowi MCP mogą faktycznie działać wewnątrz infrastruktury firmy — uruchamiać procesy, analizować logi, zarządzać usługami — bez narażania bezpieczeństwa i bez potrzeby tworzenia kosztownych integracji.</p>
<p>To otwiera zupełnie nowe scenariusze użycia, szczególnie w środowiskach o wysokim poziomie kontroli, gdzie dostęp do systemów musi być ściśle ograniczony.</p>
<p>Jakie zastosowania dla MCP widzicie w Waszych systemach? Czy przydałby się Wam agent AI, który restartuje usługi w środowisku, monitoruje logi albo generuje raporty po wdrożeniach?</p>
<p>Dajcie znać w komentarzach lub odezwijcie się do mnie bezpośrednio — chętnie porozmawiam o konkretnych pomysłach!</p>
]]></content:encoded></item><item><title><![CDATA[Jak charakterystyki architektoniczne mogą kształtować obsługę wyjątków w .NET]]></title><description><![CDATA[W świecie .NET można wyróżnić wiele sposobów obsługi błędów. Należą do nich: rzucanie wyjątków, metody TryXXX, obiekty Result, a nawet mechanizmy oparte na typach OneOf. Choć temat jest znany i opisywany od lat, wiele decyzji dotyczących ich obsługi ...]]></description><link>https://beniaminlenarcik.pl/jak-charakterystyki-architektoniczne-moga-ksztaltowac-obsluge-wyjatkow-w-net</link><guid isPermaLink="true">https://beniaminlenarcik.pl/jak-charakterystyki-architektoniczne-moga-ksztaltowac-obsluge-wyjatkow-w-net</guid><category><![CDATA[odporność]]></category><category><![CDATA[utrzymywalność]]></category><category><![CDATA[prostota]]></category><category><![CDATA[wymaganianiefunkcjonalne]]></category><category><![CDATA[charakterystykiarchitektoniczne]]></category><category><![CDATA[driveryarchitektoniczne]]></category><category><![CDATA[wyjątki]]></category><category><![CDATA[.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[exceptions]]></category><category><![CDATA[Architecture Design]]></category><category><![CDATA[resilency]]></category><category><![CDATA[maintainability]]></category><category><![CDATA[simplicity]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Fri, 09 May 2025 14:38:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746801405514/ebfa7da9-35d3-4133-935c-f257354513bf.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>W świecie .NET można wyróżnić wiele sposobów obsługi błędów. Należą do nich: rzucanie wyjątków, metody TryXXX, obiekty Result, a nawet mechanizmy oparte na typach OneOf. Choć temat jest znany i opisywany od lat, wiele decyzji dotyczących ich obsługi podejmujemy bez świadomości, że są to decyzje architektoniczne.</p>
<p>W zależności od rodzaju systemu, jego etapu rozwoju oraz otoczenia biznesowego, jedne charakterystyki architektoniczne mogą dominować nad pozostałymi. To właśnie je powinniśmy brać pod uwagę podczas wyboru stylu obsługi błędów. Oznacza to, że nie ma jednego słusznego rozwiązania, a wybór zależy od tego, co chcemy osiągnąć jako projekt.</p>
<p>W artykule przedstawię różne podejścia do zarządzania nieoczekiwanym stanem w C#, zwracając uwagę na to, jak wybory mogą wspierać wymagania niefunkcjonalne naszej aplikacji.</p>
<h1 id="heading-wymagania-niefunkcjonalne-a-podejscie-do-bledow">Wymagania niefunkcjonalne, a podejście do błędów</h1>
<h2 id="heading-resiliency-odpornosc">Resiliency (odporność)</h2>
<p>Posłużmy się prostym systemem śledzenia przesyłek, w którym klienci sprawdzają status swoich paczek. Aby ograniczyć obciążenie systemu, dane o przesyłkach są przechowywane w cache z czasem życia ustawionym na jedną godzinę.</p>
<p>Jeśli podczas odświeżania pamięci podręcznej wystąpi błąd (np. przez chwilową niedostępność zewnętrznej usługi), system nie powinien przerywać działania. Zamiast tego może:</p>
<ul>
<li><p>Zwrócić wartość domyślną lub pusty wynik.</p>
</li>
<li><p>Zarejestrować problem i użyć nieaktualnych danych.</p>
</li>
<li><p>Podjąć próbę naprawy.</p>
</li>
</ul>
<p>W rozwiązaniach stawiających na resiliency kluczowe jest unikanie nadmiernego używania wyjątków, co pozwala świadomie decydować o sposobie reakcji na problem i zapewnia większą elastyczność działań naprawczych.</p>
<p><strong>Zamiast przerywać działanie:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ExternalServiceUnavailableException();
</code></pre>
<p><strong>Można zareagować w kontrolowany sposób:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (!_shipmentApi.TryRefreshShipmentStatus(trackingId, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> shipmentStatus))
{
    <span class="hljs-keyword">return</span> Result.Fail(<span class="hljs-string">"Shipment status could not be refreshed."</span>);
}

<span class="hljs-keyword">return</span> Result.Ok(shipmentStatus);
</code></pre>
<p><strong>Albo podjąć działanie naprawcze:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> cachedData = _cache.GetShipmentStatus(trackingId);
<span class="hljs-keyword">if</span> (cachedData.IsExpired &amp;&amp; !_shipmentApi.TryRefreshShipmentStatus(trackingId, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> shipmentStatus))
{
    Log.Warning(<span class="hljs-string">"Failed to refresh expired shipment status. Using cached data."</span>);
    <span class="hljs-keyword">return</span> cachedData;
}

<span class="hljs-keyword">return</span> shipmentStatus ?? cachedData;
</code></pre>
<h2 id="heading-maintainability-utrzymywalnosc">Maintainability (utrzymywalność)</h2>
<p>Pomyślmy o starym, wielki system finansowy w stadium utrzymania.</p>
<p>Programiści, którzy pracują przy jego łataniu, zdecydowaną większość czasu spędzają na czytaniu istniejącego kodu i szukaniu źródeł problemów. Żeby usprawnić proces, możemy:</p>
<ul>
<li><p>Weryfikować poprawność danych jak najbliżej punktu wejścia oraz jasno sygnalizować wykryte problemy.</p>
</li>
<li><p>Jasno komunikować, dlaczego dane nie mogą zostać przetworzone.</p>
</li>
<li><p>Udostępniać informacje, które pomogą w namierzeniu nieprawidłowości.</p>
</li>
</ul>
<p>W legacy, zamiast pozwalać na propagację nieprawidłowego stanu i ręczną diagnostykę na podstawie wyjątków systemowych takich jak NullReferenceException w zestawieniu z ręcznie wyszukiwanymi danymi, staramy się maksymalnie uprościć szukanie przyczyny problemów.</p>
<p><strong>Zamiast pozwalać na ciche błędy:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> <span class="hljs-title">GetClientAddress</span>(<span class="hljs-params">Guid clientId</span>)</span>
{
    <span class="hljs-keyword">var</span> client = GetClientDetails(clientId);
    <span class="hljs-keyword">return</span> client.Address;
}
</code></pre>
<p><strong>Lepiej jasno i od razu sygnalizować problem:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> <span class="hljs-title">GetClientAddress</span>(<span class="hljs-params">Guid clientId</span>)</span>
{
    <span class="hljs-keyword">var</span> client = GetClientDetails(clientId);
    <span class="hljs-keyword">if</span> (client <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ClientNotFoundException(<span class="hljs-string">$"Client details could not be retrieved. Client ID: <span class="hljs-subst">{clientId}</span>"</span>);

    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrWhiteSpace(client.Address))
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidClientDataException(<span class="hljs-string">$"Client address is missing. Client ID: <span class="hljs-subst">{clientId}</span>"</span>);

    <span class="hljs-keyword">return</span> client.Address;
}
</code></pre>
<h2 id="heading-simplicity-prostota">Simplicity (prostota)</h2>
<p>Wyobraźmy sobie szybki projekt MVP — aplikację do zamawiania kawy online, która ma jedynie sprawdzić zainteresowanie użytkowników przed rozbudową pełnej platformy.</p>
<p>W projekcie typu MVP lub PoC celowo rezygnujemy z rozbudowanej walidacji, koncentrując się na szybkim dostarczeniu wartości zamiast perfekcyjnej obsługi wszystkich przypadków. Jeśli coś pójdzie nie tak, aplikacja może po prostu zakończyć działanie błędem, co jest akceptowalne na tym etapie rozwoju. W takim podejściu możemy:</p>
<ul>
<li><p>Nie sprawdzać przypadków negatywnych — ewentualne błędy zostaną wykryte naturalnie przez „wykrzaczenie” systemu.</p>
</li>
<li><p>Rzucać proste wyjątki, gdy napotkamy problem, bez tworzenia rozbudowanej hierarchii wyjątków.</p>
</li>
</ul>
<p><strong>Zamiast tworzyć walidację i własne wyjątki:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> Order <span class="hljs-title">CreateOrder</span>(<span class="hljs-params">OrderRequest request</span>)</span>
{
    <span class="hljs-keyword">if</span> (request == <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidOrderException(<span class="hljs-string">"Order request is null."</span>);

    <span class="hljs-keyword">if</span> (request.ProductId == <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidOrderException(<span class="hljs-string">"Product ID is missing."</span>);

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Order(request.ProductId, request.Quantity);
}
</code></pre>
<p><strong>Lepiej pozwolić systemowi samemu zgłosić problem:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> Order <span class="hljs-title">CreateOrder</span>(<span class="hljs-params">OrderRequest request</span>)</span>
{ 
    <span class="hljs-comment">// Brak walidacji, jeśli coś pójdzie nie tak, pojawi się naturalny wyjątek</span>
    <span class="hljs-keyword">return</span> _orders.First();
}
</code></pre>
<p><strong>Jeśli już reagujemy, to prosto i bez zbędnych klas wyjątków:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (!isValid)
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidOperationException(<span class="hljs-string">"Invalid request"</span>);
</code></pre>
<h2 id="heading-performance-wydajnosc">Performance (wydajność)</h2>
<p>W .NET 9 wprowadzono istotne usprawnienia w mechanizmie obsługi wyjątków, znacząco poprawiając jego wydajność. Nowa implementacja opiera się na architekturze NativeAOT, co zauważalnie obniża koszt obsługi wyjątków, szczególnie w prostych blokach catch i podczas operacji asynchronicznych. Szczegóły tych zmian można znaleźć w oficjalnej dokumentacji Microsoft:<br /><a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#faster-exceptions">What's new in .NET 9 – Runtime Improvements</a>.</p>
<p>Mimo tych optymalizacji wyjątki nadal przerywają przepływ wykonania, co może utrudniać optymalizację kodu i negatywnie wpływać na jego przewidywalność, zwłaszcza w sekcjach krytycznych pod względem wydajności. Dlatego w takich miejscach warto:</p>
<ul>
<li><p>Unikać wyjątków (np. w pętlach, przy parsowaniu, w przetwarzaniu równoległym).</p>
</li>
<li><p>Stosować metody TryXXX lub prostą walidację, by zachować płynność wykonania.</p>
</li>
</ul>
<p><strong>Przykład do zastosowania w kodzie krytycznym wydajnościowo:</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (!<span class="hljs-keyword">decimal</span>.TryParse(input, <span class="hljs-keyword">out</span> <span class="hljs-keyword">var</span> <span class="hljs-keyword">value</span>))
<span class="hljs-keyword">return</span> Result.Fail(<span class="hljs-string">$"Not parsed into decimal: <span class="hljs-subst">{input}</span>"</span>);
</code></pre>
<h1 id="heading-podsumowanie">Podsumowanie</h1>
<p>Nie ma jednej, uniwersalnej strategii obsługi wyjątków — każda decyzja powinna wynikać ze świadomego wyboru, zgodnego z charakterystykami architektonicznymi systemu. Raz będzie to prostota, innym razem pełna kontrola nad poprawnością danych, a jeszcze innym — odporność systemu na nieprzewidziane sytuacje.</p>
<p><strong>A Ty? Jakie podejście najczęściej stosujesz w swoich projektach?</strong></p>
<ul>
<li><p>Pozwalasz, by wyjątki systemowe propagowały się dalej?</p>
</li>
<li><p>Rzucasz własne wyjątki z dodatkowymi informacjami?</p>
</li>
<li><p>Stosujesz wzorzec Result zamiast wyjątków?</p>
</li>
<li><p>A może próbujesz automatycznie naprawiać dane i kontynuować przetwarzanie?</p>
</li>
</ul>
<p><strong>Czy przy wyborze sposobu obsługi błędów uwzględniasz charakterystyki architektoniczne swojego systemu?</strong></p>
]]></content:encoded></item><item><title><![CDATA[Nowoczesny klasyfikator komend w aplikacji mobilnej]]></title><description><![CDATA[W ostatnich tygodniach pracowałem nad modułem rozpoznawania i klasyfikacji komend głosowych w mojej aplikacji mobilnej Jokes Portal. Celem było stworzenie interfejsu, który umożliwia pełną, bezdotykową interakcję z aplikacją – od przeszukiwania żartó...]]></description><link>https://beniaminlenarcik.pl/nowoczesny-klasyfikator-komend-w-aplikacji-mobilnej</link><guid isPermaLink="true">https://beniaminlenarcik.pl/nowoczesny-klasyfikator-komend-w-aplikacji-mobilnej</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[classification]]></category><category><![CDATA[#maui]]></category><category><![CDATA[jokes]]></category><category><![CDATA[#JokesPortal]]></category><category><![CDATA[AI]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Thu, 24 Apr 2025 13:46:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745513339777/2fb1c262-5d62-4e54-9452-59afd6dcabaf.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>W ostatnich tygodniach pracowałem nad modułem rozpoznawania i klasyfikacji komend głosowych w mojej aplikacji mobilnej <a target="_blank" href="https://www.jokesportal.com/"><strong>Jokes Portal</strong></a>. Celem było stworzenie interfejsu, który umożliwia pełną, bezdotykową interakcję z aplikacją – od przeszukiwania żartów po ich odtwarzanie na głos. Taka funkcja nie tylko zwiększa komfort użytkowania, ale wnosi też poczucie nowoczesności i może dotrzeć do nowych odbiorców: kierowców, uczestników spotkań towarzyskich czy użytkowników z ograniczoną sprawnością. Co szczególnie ważne, rozwiązanie to wpisuje się w zasady budowania nowoczesnych produktów opartych na AI – wspierając inkluzywność. Osoby niewidome czy nieumiejące czytać i pisać zyskują pełny dostęp do treści aplikacji, czyli żartów.</p>
<h2 id="heading-jakie-mialem-opcje-krotki-przeglad-dostepnych-rozwiazan">Jakie miałem opcje? Krótki przegląd dostępnych rozwiązań</h2>
<p>Ponieważ <a target="_blank" href="https://www.jokesportal.com/"><strong>Jokes Portal</strong></a> to pierwsza aplikacja mobilna, którą rozwijam, a technologie AI rozwijają się w zawrotnym tempie, wybór odpowiedniego rozwiązania do klasyfikacji komend nie był oczywisty. Z wiedzą, którą miałem w tamtym momencie, znalazłem trzy i pół realnej możliwości wdrożenia klasyfikatora w środowisku .NET MAUI:</p>
<ol>
<li><p><strong>Wykorzystanie dużego modelu językowego (LLM)</strong> – np. GPT-4o mini od OpenAI, z przygotowanym promptem klasyfikacyjnym.</p>
</li>
<li><p><strong>Zainstalowanie małego modelu językowego (SLM)</strong> bezpośrednio w aplikacji mobilnej, dostrojonego do komend.</p>
<ul>
<li><strong>2.5</strong>: Osadzenie takiego samego klasyfikatora w API hostowanym na Azure</li>
</ul>
</li>
<li><p><strong>Użycie klasycznego rozwiązania TF-IDF (Term Frequency – Inverse Document Frequency)</strong>, działającego lokalnie na telefonie użytkownika.</p>
</li>
</ol>
<h2 id="heading-czego-naprawde-wymaga-dobra-obsluga-komend-glosowych">Czego naprawdę wymaga dobra obsługa komend głosowych?</h2>
<p>Tworząc aplikację B2C (business to consumer), musiałem pogodzić możliwości techniczne z oczekiwaniami użytkowników. Decyzja o wyborze podejścia do klasyfikatora była podyktowana kilkoma kluczowymi czynnikami:</p>
<ul>
<li><p><strong>Szybkość reakcji</strong> – chociaż użytkownicy są przyzwyczajeni do tego, że na rezultaty generowane przez AI trzeba chwilę poczekać, w przypadku komend oczekują działania natychmiastowego. Rozpoznawanie powinno odbywać się bez zauważalnych opóźnień, najlepiej w czasie rzeczywistym.</p>
</li>
<li><p><strong>Koszt operacyjny</strong> – w aplikacji konsumenckiej każde zapytanie do zewnętrznego API (np. OpenAI) generuje koszt. Co więcej, liczba komend sterujących aplikacją (np. „pokaż ulubione”, „podoba mi się”) może być znacznie większa niż zapytań, które faktycznie wyświetlają treść. To oznacza, że ich obsługa musi być jak najtańsza, aby nie wymuszać na użytkowniku oglądania większej liczby reklam.</p>
</li>
<li><p><strong>Możliwość działania offline</strong> – aplikacja mobilna powinna działać także przy ograniczonym dostępie do Internetu, np. w podróży. To naturalne oczekiwanie użytkowników, szczególnie w kontekście aplikacji rozrywkowej.</p>
</li>
<li><p><strong>Rozmiar modelu</strong> – artefakt nie może ważyć setek megabajtów, bo użytkownicy niechętnie pobierają ciężkie aplikacje. Dodatkowo, gdy zaczyna brakować miejsca w pamięci telefonu, duże aplikacje są pierwsze do usunięcia.</p>
</li>
<li><p><strong>Obsługa wielu języków</strong> – już na starcie aplikacja wspierała polski i angielski, więc każde rozwiązanie musiało działać równie dobrze dla obu języków. To kluczowe dla komfortu użytkownika i możliwości rozwoju aplikacji.</p>
</li>
</ul>
<h2 id="heading-tf-idf-jako-model-onnx">TF-IDF jako model ONNX</h2>
<p>TF-IDF (Term Frequency – Inverse Document Frequency) to klasyczna technika przetwarzania języka naturalnego, która pozwala przekształcić tekst na wektor liczbowy. W tym podejściu każda komenda zostaje reprezentowana jako wektor ważonych słów, gdzie wagi odzwierciedlają znaczenie danego słowa w kontekście całego zbioru komend.</p>
<p>W praktyce przygotowuję zestaw reprezentatywnych komend (np. „pokaż ulubione”, „powiedz żart o zwierzętach”) i przypisuję im etykiety klas (intencji). Następnie trenuję model, który potrafi przypisać nową wypowiedź użytkownika do jednej z wcześniej zdefiniowanych intencji. Całość eksportuję do formatu ONNX, co pozwala używać tego modelu lokalnie na urządzeniu mobilnym w środowisku .Net.</p>
<p>Dla każdego języka (np. polski, angielski) tworzony jest oddzielny model. Przetwarzanie działa błyskawicznie (inferencja poniżej 10 ms), nie wymaga dostępu do Internetu i ma minimalny rozmiar (mniejszy niż 0.5 MB), co sprawia że można osadzić modele wewnątrz pakietu aplikacji.</p>
<p>Minusem rozwiązania jest to, że klasyfikator nie zwraca prawdopodobieństw – tylko etykietę intencji. Dlatego trzeba osobno zadbać o sytuację, w której użytkownik powie coś zupełnie niespodziewanego. W tym celu dodaję do zbioru treningowego specjalną klasę „unknown” z przykładami losowych, niezwiązanych komend (np. „jaka jest pogoda”, „co to jest JSON”), by model mógł nauczyć się odrzucać niepasujące zapytania.</p>
<p>Dodatkowym ograniczeniem TF-IDF jest brak rozumienia semantyki. Model opiera się wyłącznie na częstości występowania słów i ich wagach, bez rozpoznawania znaczenia kontekstu. Oznacza to, że rozwiązanie może mieć trudności z rozróżnieniem podobnych, ale znaczeniowo różnych komend, takich jak:</p>
<ul>
<li><p>„opowiedz losowy żart” (czyli: jakikolwiek)</p>
</li>
<li><p>„opowiedz żart o losowości liczb” (czyli: na temat teorii prawdopodobieństwa)</p>
</li>
</ul>
<p>Tego typu przypadki brzegowe trzeba odpowiednio obsłużyć na etapie trenowania – np. dodając więcej przykładów, które rozbijają podobnie brzmiące intencje na osobne klasy.</p>
<h2 id="heading-small-language-model-slm-lokalnie-onnx">Small Language Model (SLM) lokalnie (ONNX)</h2>
<p>Małe modele językowe (Small Language Models – SLM), takie jak MobileBERT, pozwalają na znacznie lepsze rozumienie kontekstu niż klasyczne podejścia oparte na słownikach. W nich trening odbywa się na przykładach komend użytkowników, a rozwiązanie wyeksportowane do formatu ONNX umożliwia lokalne uruchamianie na urządzeniu mobilnym w środowisku .Net.</p>
<p>SLM rozpoznaje nie tylko dokładne sformułowania, ale też różne wariacje językowe, np.:</p>
<ul>
<li><p>„powiedz coś o lekarzu”</p>
</li>
<li><p>„opowiedz żart, w którym jest doktor”</p>
</li>
</ul>
<p>Model działa w pełni offline, co spełnia jeden z kluczowych wymogów. Inferencja trwa zwykle 20–50 ms. Minusem tego podejścia jest rozmiar modelu co najmniej 70mb, co wpływa znacząco na wielkość paczki aplikacji. Aby zredukować ten problem, można rozważyć pobieranie modelu jako pakietu językowego na żądanie – z blob storage, który wykorzystywany jest już do dostarczania żartów w postaci audio.</p>
<p>W tym podejściu każdy język wymaga osobnego modelu – w moim przypadku oznacza to przetrenowanie wersji polskiej i angielskiej. W praktyce okazuje się to sporym ograniczeniem. Zdecydowana większość dostępnych modeli typu SLM została wytrenowana na języku angielskim. Dla języka polskiego istnieją modele takie jak PolBERT czy HerBERT, ale ważą one ponad 300 MB. Są zbyt ciężkie dla urządzeń mobilnych bez agresywnej optymalizacji lub ograniczania funkcji, co utrudniałoby implementację lub nawet okazało się niemożliwe do rozwiązania. Jeśli udałoby się sprostać wyzwaniom, można spodziewać się, że efekt końcowy oferuje znacznie większą odporność na przypadki brzegowe, nieoczywiste sformułowania oraz błędy rozpoznania tekstu mówionego niż TF-IDF.</p>
<p>Warto również wspomnieć, że modele BERT w formacie ONNX nie zawierają wbudowanej logiki tokenizacji. Przed wywołaniem modelu tekst musi zostać odpowiednio przetworzony – podzielony na tokeny zgodnie z oryginalną architekturą. W przypadku .NET można skorzystać z <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.ml.tokenizers.berttokenizer?view=ml-dotnet-preview">BertTokenizer</a>, który jest gotowym tokenizerem zgodnym z BERT-em. To dodatkowy krok, który należy zaimplementować ręcznie przed wykonaniem inferencji.</p>
<h2 id="heading-slm-jako-mikroserwis-w-api">SLM jako mikroserwis w API</h2>
<p>Zamiast osadzać model językowy bezpośrednio w aplikacji mobilnej, można uruchomić go jako mikroserwis w API. W tym podejściu model (np. MobileBERT wytrenowany i wyeksportowany do formatu ONNX) zostaje osadzony w aplikacji serwerowej – <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core API działającym na Azure App Service, które już teraz obsługuje backend aplikacji mobilnej. Artefakty ONNX o rozmiarze 100–200 MB, a nawet większym, mogą być bez problemu ładowane do pamięci przy starcie serwera.</p>
<p>Największą zaletą tego podejścia jest brak wpływu na rozmiar aplikacji mobilnej – klient nie musi pobierać dużych modeli, a przetwarzanie odbywa się po stronie backendu. Łatwiejsze jest również wersjonowanie i aktualizacja modeli – nie wymaga to publikacji nowej wersji aplikacji w Google Play.</p>
<p>Jeśli budżet na infrastrukturę na to pozwala, mikroserwis można uruchomić na dedykowanej instancji lub w osobnym kontenerze, co zwiększa jego skalowalność i pozwala izolować komponent odpowiedzialny za przetwarzanie języka.</p>
<p>W porównaniu do podejścia lokalnego tracimy możliwość działania offline. Dochodzą też koszty związane z transferem danych i utrzymaniem backendu. Zyskujemy natomiast minimalny rozmiar aplikacji oraz elastyczność zarządzania modelem.</p>
<h2 id="heading-openai-gpt-jako-klasyfikator-intencji">OpenAI / GPT jako klasyfikator intencji</h2>
<p>Podejście oparte o GPT (np. OpenAI GPT-4o mini) oferuje najwyższą jakość klasyfikacji i największą elastyczność językową. Użytkownik może powiedzieć „coś śmiesznego o lekarzach i chomikach” i model z dużym prawdopodobieństwem rozpozna kontekst i przekaże go dalej jako intencję.</p>
<p>Największą zaletą tego podejścia jest błyskawiczne wdrożenie – nie trzeba trenować własnego modelu, wystarczy przygotować odpowiedni prompt. Dodatkowo każde zapytanie może być logowane i analizowane, co pozwala na stopniowe zbieranie danych do późniejszego trenowania własnego rozwiązania dostrojonego do aplikacji. Z drugiej strony, zastosowanie wymaga aktywnego połączenia z Internetem, a każde wywołanie generuje koszt.</p>
<h2 id="heading-porownanie-wszystkich-podejsc">Porównanie wszystkich podejść</h2>
<p>Poniższa tabela porównuje pięć podejść do klasyfikacji komend głosowych dla przypadku użycia w aplikacji <a target="_blank" href="https://www.jokesportal.com/en">Jokes Portal</a>. Oceny zostały przyznane w skali od 0 (najgorzej) do 3 (najlepiej). Wartość 0 oznacza, że dane podejście w tej chwili zupełnie nie spełnia wymagań – jest "show stopperem".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745502228455/22d73b75-b2bc-4532-be62-6ba3c279f214.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-co-wybralem-i-dlaczego">Co wybrałem i dlaczego</h2>
<p>Po porównaniu wszystkich podejść i skonfrontowaniu ich z rzeczywistymi wymaganiami aplikacji mobilnej <a target="_blank" href="https://www.jokesportal.com/">Jokes Portal</a>, wybór padł na klasyczny model <strong>TF-IDF wytrenowany i zapisany jako artefakt ONNX</strong>. Choć nie oferuje on zaawansowanego rozumienia języka, okazał się najbardziej opłacalny i praktyczny. Działa błyskawicznie, nie wymaga połączenia z Internetem, ma śladowy rozmiar i może być łatwo wdrażany oddzielnie dla każdego języka.</p>
<p>Rozwiązania oparte o modele językowe (SLM) lokalnie lub przez blob storage zapewniają większą elastyczność, ale obecnie są zbyt ciężkie (zwłaszcza dla języka polskiego). SLM jako mikroserwis był rozważany jako potencjalne rozwiązanie, jednak przegrywa z modelem dostarczanym przez blob storage pod względem kosztów, elastyczności i prostoty wdrożenia. Dodatkowo nie spełnia kluczowego wymagania działania offline. Z kolei OpenAI, mimo świetnych rezultatów i najwyższej jakości rozpoznawania języka, nie działa bez połączenia z internetem i generuje koszt przy każdym zapytaniu. W kontekście aplikacji opartej na modelu reklamowym jest to zbyt drogie do zastosowania na większą skalę.</p>
<p>TF-IDF (Term Frequency – Inverse Document Frequency) nie jest rozwiązaniem doskonałym, ale w połączeniu z odpowiednim zestawem treningowym spełnia wszystkie kluczowe wymagania użytkowe i techniczne. Na tym etapie rozwoju aplikacji i otaczających nas technologii uważam, że jest to po prostu najlepszy możliwy wybór.</p>
<p>Temat klasyfikacji komend jest innowacyjny i jak każde rozwiązania oparte o AI szybko się rozwija. Jeśli widzisz inne podejścia, masz doświadczenia z podobnymi problemami albo po prostu chcesz podyskutować, wspólnie przemyśleć podobne rozwiązanie – śmiało napisz do mnie lub zostaw komentarz.</p>
]]></content:encoded></item><item><title><![CDATA[Jak skutecznie wyszukiwać wiele elementów w kolekcjach .NET]]></title><description><![CDATA[W dzisiejszym świecie rozproszonych systemów, mikro serwisów i różnych sposobów przechowywania danych coraz częściej musimy łączyć dane w warstwie obliczeniowej aplikacji. Do poszukiwania relacji między nimi często korzystamy z właściwości takich jak...]]></description><link>https://beniaminlenarcik.pl/jak-skutecznie-wyszukiwac-wiele-elementow-w-kolekcjach-net</link><guid isPermaLink="true">https://beniaminlenarcik.pl/jak-skutecznie-wyszukiwac-wiele-elementow-w-kolekcjach-net</guid><category><![CDATA[#JokesPortal]]></category><category><![CDATA[Kolekcje]]></category><category><![CDATA[Wyszukiwanie]]></category><category><![CDATA[.NET]]></category><category><![CDATA[linq]]></category><category><![CDATA[hashset]]></category><category><![CDATA[Collections]]></category><category><![CDATA[Contains]]></category><category><![CDATA[Benchmark]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Mon, 17 Mar 2025 22:30:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742251457191/a8326ac6-18db-44bd-9c5c-bdab550cf5ee.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>W dzisiejszym świecie rozproszonych systemów, mikro serwisów i różnych sposobów przechowywania danych coraz częściej musimy łączyć dane w warstwie obliczeniowej aplikacji. Do poszukiwania relacji między nimi często korzystamy z właściwości takich jak globalnie unikalne identyfikatory (GUID), wartości liczbowe czy inne specyficzne pola, np. nazwy produktów.</p>
<p>Jako praktyczny przykład prezentuję schemat architektoniczny mojej aplikacji mobilnej <a target="_blank" href="https://play.google.com/store/apps/details?id=com.JokesPortal.JokesMobile.Client">JokesPortal</a><em>,</em> w której często spotykam się z koniecznością łączenia danych z różnych źródeł - systemów przechowywania i produktów 3rd party:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742243678281/cf0bd9b2-84ea-45b0-983a-3e1c7d09547b.png" alt class="image--center mx-auto" /></p>
<p>Podczas łączenia dużych zbiorów danych łatwo przeoczyć jedno z najczęstszych źródeł utraty wydajności – wielokrotne wyszukiwanie elementów jednej kolekcji w drugiej.</p>
<p>W artykule pokażę jak skutecznie wykonywać agregację elementów między kolekcjami w warstwie obliczeniowej. Podzielę się z Tobą obrazową analogią oraz praktycznym, krótkim benchmarkiem.</p>
<h1 id="heading-analogia-dostawcy-jedzenia">Analogia dostawcy jedzenia</h1>
<p>Wyobraź sobie, że pracujesz jako dostawca jedzenia w znanej firmie, w której najczęściej Twoim pojazdem służbowym jest rower elektryczny lub skuter. Masz do dostarczenia pełną torbę zamówień do największego bloku w mieście - 10 000 mieszkań.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742251353424/fbbc6abb-56a3-4e6d-81f5-7deaa357ad46.png" alt class="image--center mx-auto" /></p>
<p>W swojej aplikacji mobilnej widzisz wszystkie potrzebne informacje, w tym numery lokali pod którymi głodni klienci czekają na dostawę. Gdy wchodzisz do budynku, czeka na Ciebie przykra niespodzianka. Blok jest na tyle nowy, że zarządca nie umieścił jeszcze numerów mieszkań na drzwiach i korytarzach.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742251508782/525b75e4-eb77-4845-9bc5-33f73924c45a.png" alt class="image--center mx-auto" /></p>
<p>Aby dostarczyć zamówienie, musisz pukać do każdych drzwi pytając o numer lokalu i porównując go z odpowiednikami na opakowaniach, które masz w torbie. Jesteś zbyt zabiegany, by choćby posortować paczki z jedzeniem.</p>
<p>Ta niewesoła sytuacja jest dokładnym odpowiednikiem wywoływania metody <code>.Contains()</code> na liście w każdej iteracji pętli. Każdorazowo przeszukujesz torbę z jedzeniem w poszukiwaniu konkretnego numeru zamówienia. Czynność powtarzasz dla każdych drzwi w bloku.</p>
<h2 id="heading-optymalne-rozwiazanie-uzycie-hashset-lub-dictionary">Optymalne rozwiązanie: użycie <code>HashSet</code> lub <code>Dictionary</code>.</h2>
<p>Z reguły mieszkania w budynku są oznaczone numerami. Dodatkowo, na każdym piętrze, a może przy wejściu do budynku może znajdować się lista z rozpiską: piętro - numery mieszkań. Dzięki temu możesz skierować się od razu do właściwego mieszkania.</p>
<p>Jeśli kilka zamówień trafia pod jeden adres lub na to samo piętro, warto na chwilę zatrzymać się przy tablicy z numerami mieszkań i uporządkować torbę. Może to zaoszczędzić sporo czasu.</p>
<p>W .Net analogiczną, właściwą optymalizację dla takiego przypadku użycia oferują struktury danych takie jak <code>HashSet</code> lub <code>Dictionary</code>. Korzystają one z indeksu na podstawie hashów. Umożliwiają błyskawiczny dostęp i eliminują konieczność niepotrzebnego przeszukiwania całych kolekcji. Dodatkowo, algorytmy oparte na indeksach gwarantują stały czas wyszukiwania, w przeciwieństwie do wyszukiwania liniowego, które może się znacznie różnić w zależności od rozkładu danych.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742303554879/a0694149-3b68-418f-9e3b-6935872284c8.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-benchmark-metod-contains-i-select">Benchmark metod .Contains() i .Select()</h1>
<p>Aby pokazać różnice wydajności pomiędzy różnymi podejściami do wyszukiwania wielu elementów w kolekcjach, przygotowałem praktyczny benchmark. Porównałem kilka metod, które często stosujemy w aplikacjach .NET. Poniżej przedstawiam wyniki tych pomiarów. Eksperyment przeprowadziłem w środowisku .Net9.</p>
<p>Kod źródłowy dostępny na moim <a target="_blank" href="https://github.com/L3mur1/NetBenchmarkCollectionLookups">GitHub</a>.</p>
<h2 id="heading-konfiguracja">Konfiguracja</h2>
<p>Porównałem kilka typowych scenariuszy wyszukiwania w celu zmierzenia wydajności wyszukiwania wielu elementów w kolekcji:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Testowany przypadek</td><td>Struktura danych i metoda</td><td>Opis działania</td><td>Złożoność</td></tr>
</thead>
<tbody>
<tr>
<td>Contains_Multiple_Dictionary</td><td>Dictionary.ContainsKey</td><td>Tworzenie słownika przy każdym wyszukiwaniu.</td><td>O(n + m)</td></tr>
<tr>
<td>Contains_Multiple_Dictionary_Precomputed</td><td>Dictionary.ContainsKey (precomputed)</td><td>Słownik utworzony raz, używany wielokrotnie.</td><td>O(m)</td></tr>
<tr>
<td>Contains_Multiple_HashSet</td><td>HashSet.Contains</td><td>Tworzenie HashSetu przy każdym wyszukiwaniu.</td><td>O(n + m)</td></tr>
<tr>
<td>Contains_Multiple_HashSet_Precomputed</td><td>HashSet.Contains (precomputed)</td><td>HashSet utworzony raz, używany wielokrotnie.</td><td>O(m)</td></tr>
<tr>
<td>Contains_Multiple_List_Any</td><td>List.Any</td><td>Przeszukiwanie liniowe dla każdego wyszukiwania.</td><td>O(n × m)</td></tr>
<tr>
<td>Select_Multiple_Dictionary</td><td>Dictionary.TryGetValue (tworzony za każdym razem)</td><td>Tworzenie słownika przy każdym wyszukiwaniu, TryGetValue zamiast ContainsKey.</td><td>O(n + m)</td></tr>
<tr>
<td>Select_Multiple_List_FirstOrDefault</td><td>List.FirstOrDefault</td><td>Przeszukiwanie liniowe, zwracanie pierwszego pasującego elementu</td><td>O(n × m)</td></tr>
</tbody>
</table>
</div><h2 id="heading-wyniki">Wyniki</h2>
<p>Poniżej przedstawiam wyniki benchmarku wyszukiwania 1000 elementów w kolekcji liczącej 10 000 elementów (środowisko .NET 9):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742249320954/7cfc176f-d132-4a46-bb03-68c3975b5e49.png" alt class="image--center mx-auto" /></p>
<p>Przedstawione wyniki dotyczą konkretnego przypadku testowego – w praktyce warto powtórzyć benchmarki, dostosowując rozmiary kolekcji do własnych potrzeb.</p>
<h1 id="heading-wnioski">Wnioski</h1>
<p>Benchmark wyraźnie pokazuje, że przy wielokrotnych wyszukiwaniach warto przekształcić kolekcję do <code>Dictionary</code> lub <code>HashSet</code>. Należy unikać <code>.Contains()</code> i innych operacji liniowych na listach w pętlach.</p>
<h2 id="heading-na-liczbach">Na liczbach</h2>
<p>Przy potrzebie pojedynczej iteracji przez kolekcję, zamiana na słownik (<code>Contains_Multiple_Dictionary</code>) daje wzrost wydajności o <strong>~14x</strong> w stosunku do korzystania z listy (<code>Contains_Multiple_List_Any</code>).</p>
<p><code>374.165 / 245.24​ = 14.02</code> ~ 1 400%</p>
<p>Gdy wymagane jest kilka przejść przez pętlę, wydajność <code>Precomputed Dictionary</code> sprawia, że kolejne przejścia nie mają istotnego wpływu na redukcję wydajności, każde kolejne wyszukiwanie jest <strong>~365x szybsze</strong> w porównaniu do użycia listy <code>Contains_Multiple_List_Any</code></p>
<p><code>5245.24​ / 14.35 = 365.52</code> ~ 36 500%</p>
<h2 id="heading-przyklad-jak-skutecznie-wyszukac-wiele-elementow-w-liscie">Przykład, jak skutecznie wyszukać wiele elementów w liście.</h2>
<p>To przykład zamiany listy w słownik przy wyszukiwaniu elementów o takim samym ID(Guid) ze stworzonych <a target="_blank" href="https://github.com/L3mur1/NetBenchmarkCollectionLookups/blob/master/NetBenchmarkCollectionLookups/Benchmark/CollectionLookup/ContainsLookupBenchmark.cs">benchmarków</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742250328723/ba4e7c5e-b0b5-478b-ad3b-ad1d38ab34b9.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-uwaga">Uwaga</h3>
<p>Metoda <code>.ToDictionary()</code> wymaga unikalnych kluczy – w przypadku duplikatów rzucony zostanie wyjątek <code>ArgumentException</code>. Jeśli dane pochodzą z wielu źródeł, a duplikaty są możliwe, warto najpierw je obsłużyć (np. używając <code>.GroupBy().ToDictionary()</code>).</p>
<h2 id="heading-dodatkowe-informacje">Dodatkowe informacje</h2>
<p>Warto również pamiętać także o metodach LINQ, takich jak <code>.Intersect()</code> czy <code>.Except()</code>, które także wykorzystują haszowanie.</p>
]]></content:encoded></item><item><title><![CDATA[Snapshot Testing - Prostota i dokumentacja w jednym]]></title><description><![CDATA[Dostarczając funkcjonalność opartą na Azure Cognitive Services, w której chciałem wykorzystać funkcję czytania tekstu przez AI, zaskoczyło mnie SDK. Okazało się, że biblioteka NuGet Microsoft.CognitiveServices.Speech nie posiada własnych modeli, któr...]]></description><link>https://beniaminlenarcik.pl/snapshot-testing-prostota-i-dokumentacja-w-jednym</link><guid isPermaLink="true">https://beniaminlenarcik.pl/snapshot-testing-prostota-i-dokumentacja-w-jednym</guid><category><![CDATA[testy migawkowe]]></category><category><![CDATA[SSML]]></category><category><![CDATA[Speech Synthesis Markup Language]]></category><category><![CDATA[SSMLBuilder]]></category><category><![CDATA[SSMLBuilder.Azure]]></category><category><![CDATA[Azure]]></category><category><![CDATA[snapshot testing]]></category><category><![CDATA[verify]]></category><category><![CDATA[azure cognitive services]]></category><category><![CDATA[Azure Speech Service]]></category><category><![CDATA[AI]]></category><category><![CDATA[.NET]]></category><category><![CDATA[Testing]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Beniamin Lenarcik]]></dc:creator><pubDate>Mon, 20 Jan 2025 10:49:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737370140535/cde134d2-3d35-40b2-ac75-1c841f47ed97.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Dostarczając funkcjonalność opartą na Azure Cognitive Services, w której chciałem wykorzystać funkcję czytania tekstu przez AI, zaskoczyło mnie SDK. Okazało się, że biblioteka NuGet <a target="_blank" href="https://www.nuget.org/packages/Microsoft.CognitiveServices.Speech">Microsoft.CognitiveServices.Speech</a> nie posiada własnych modeli, które by odzwierciedlały strukturę języka znaczników syntezy mowy: <a target="_blank" href="https://learn.microsoft.com/pl-pl/azure/ai-services/speech-service/speech-synthesis-markup">Speech Synthesis Markup Language (SSML)</a>. Rozwinięcie tego tematu to na pewno pomysł na kolejny artykuł. :) W wielkim skrócie, wypuściłem w świat paczkę <a target="_blank" href="https://www.nuget.org/packages/SSMLBuilder.Azure">SSMLBuilder.Azure</a>, która adresuje te niedogodności.</p>
<h1 id="heading-jak-przetestowac-generator-struktury-xml">Jak przetestować generator struktury XML?</h1>
<p>Podczas pracy nad kodem źródłowym do tworzenia tekstu dla efektywnej syntezy mowy, zauważyłem, że większość interfejsów, czyli całe „mięso”, to w istocie tworzenie generatorów odpowiedniej struktury XML, którą jest SSML. Zacząłem zastanawiać się, jak przetestować poszczególne bloki struktury, które sprowadzały się praktycznie do implementacji poniższego interfejsu:</p>
<pre><code class="lang-csharp"> <span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">ISsmlElement</span>
    {
        <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
        <span class="hljs-comment"><span class="hljs-doctag">///</span> Converts the implementing element to an SSML-formatted element.</span>
        <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
        <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;param name="ns"&gt;</span>The XML namespace to be used for the SSML element, if needed.<span class="hljs-doctag">&lt;/param&gt;</span></span>
        <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;returns&gt;</span>An <span class="hljs-doctag">&lt;see cref="SsmlElement"/&gt;</span> that represents the current object in SSML format.<span class="hljs-doctag">&lt;/returns&gt;</span></span>
        <span class="hljs-function">SsmlElement <span class="hljs-title">ToSsml</span>(<span class="hljs-params">XNamespace? ns = <span class="hljs-literal">null</span></span>)</span>;
    }
</code></pre>
<p>czyli w praktyce zwrócenia obiektu XML:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">SsmlElement</span>
    {
        [<span class="hljs-meta">...</span>]
        <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;summary&gt;</span></span>
        <span class="hljs-comment"><span class="hljs-doctag">///</span> Gets the underlying XElement representation of this SSML element.</span>
        <span class="hljs-comment"><span class="hljs-doctag">///</span> <span class="hljs-doctag">&lt;/summary&gt;</span></span>
        <span class="hljs-keyword">public</span> XElement XElement { <span class="hljs-keyword">get</span>; }
        [<span class="hljs-meta">...</span>]
    }
</code></pre>
<h2 id="heading-asserty-xelement">Asserty XElement</h2>
<p>Pierwszym pomysłem, a tak naprawdę brakiem zrozumienia problemu, byłoby każdorazowe przygotowywanie oczekiwanego obiektu XML do porównania w assercie. W czasach GPT możemy łatwo „wyobrazić” sobie, jakby wyglądałyby Asserty w tej sytuacji i jakie byłoby ich setupowanie:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// give me c# code that will produce such content hardcoded, using XElements: [...]</span>
XNamespace ns = <span class="hljs-string">"http://www.w3.org/2001/10/synthesis"</span>;
        XElement expectedElement = <span class="hljs-keyword">new</span> XElement(ns + <span class="hljs-string">"speak"</span>,
            <span class="hljs-keyword">new</span> XAttribute(<span class="hljs-string">"version"</span>, <span class="hljs-string">"1.0"</span>),
            <span class="hljs-keyword">new</span> XAttribute(XNamespace.Xml + <span class="hljs-string">"lang"</span>, <span class="hljs-string">"en-US"</span>),
            <span class="hljs-keyword">new</span> XElement(ns + <span class="hljs-string">"voice"</span>,
                <span class="hljs-keyword">new</span> XAttribute(<span class="hljs-string">"name"</span>, <span class="hljs-string">"en-US-AvaMultilingualNeural"</span>),
                <span class="hljs-string">"Why are snails slow?"</span>
            )
        );
</code></pre>
<p>Utrzymanie tego rodzaju testów z pewnością nie jest przyjemne, a błędy w literkach mogą prowadzić do kolejnych straconych minut. Co więcej, jak sprawdzić, czy Azure Speech Service zgodnie z oczekiwaniem interpretuje ten skomplikowany markup? Wydaje się, że nie obyłoby się bez testów integracyjnych, ale takie rozwiązanie rodzi tylko kolejne pytania bez odpowiedzi. Jak przetestować nagranie dźwiękowe? Jak uruchamiać testy integracyjne przeciwko chmurze Azure w projekcie, który stworzyłem jako open source? To totalna przesada w kontekście utrzymania.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737319052792/cd6833c6-243f-4521-9da2-f376d1aec69c.webp?height=400" alt class="image--center mx-auto" /></p>
<h2 id="heading-moze-testowac-plain-text">Może testować plain text?</h2>
<p>Jeśli <a class="post-section-overview" href="#Pomys%C5%82-na-Asserty-XElement">Pomysł na Asserty XElement</a> nie wchodzi w grę, mam pomysł by porównywać rezultat generowania jako plain text. Zgodnie z dokumentacją metody <a target="_blank" href="https://learn.microsoft.com/en-us/dotnet/api/system.xml.linq.xnode.tostring?view=net-9.0">XNode.ToString()</a>, która zwraca XML dla reprezentowanego przez obiekt węzła, można użyć tej metody do generowania i porównywania stringów XML. Oto przykład takiego rozwiązania:</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Arrange</span>
            <span class="hljs-keyword">var</span> sut = Speak.InLanguage(<span class="hljs-string">"en-US"</span>)
                .WithElement(Voice
                .Named(<span class="hljs-string">"en-US-AvaMultilingualNeural"</span>)
                .WithContent(Content.Of(<span class="hljs-string">"Why are snails slow?"</span>)));

            <span class="hljs-comment">// Act</span>
            <span class="hljs-keyword">var</span> result = sut.ToString();
</code></pre>
<p>Do assercji można przygotować zahardkodowany string z oczekiwaną strukturą XML. To rozwiązanie jest lepsze, ponieważ programista widząc oczekiwaną strukturę:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> expectedResult = <span class="hljs-string">"﻿&lt;speak version="</span><span class="hljs-number">1.0</span><span class="hljs-string">" xml:lang="</span>en-US<span class="hljs-string">" xmlns="</span>http:<span class="hljs-comment">//www.w3.org/2001/10/synthesis"&gt;</span>
  &lt;voice name=<span class="hljs-string">"en-US-AvaMultilingualNeural"</span>&gt;Why are snails slow?&lt;/voice&gt;
&lt;/speak&gt;<span class="hljs-string">"</span>
</code></pre>
<p>ma możliwość wizualnej oceny struktury XML, co daje lepsze zrozumienie tego, czego dotyczy test, a także umożliwia łatwe skopiowanie wyniku lub oczekiwanego rezultatu do <a target="_blank" href="https://learn.microsoft.com/pl-pl/azure/ai-services/speech-service/speech-studio-overview">Azure Speech Studio</a> i przesłuchania wygenerowanego nagrania. Skopiowanie struktury z debuggera Visual Studio zamienia znaki „&lt;“ na „lt” ze względu na zabezpieczenie przed atakami typu injection, ale to problem można rozwiązać w dalszej pracy nad kodem. Kolejne dopracowanie tego pomysłu polega na zastąpieniu oczekiwanych rezultatów, hardkodowanych w postaci stringów elementów XML, przechowywaniem ich w osobnych plikach. Dla każdego testu można by mieć oddzielny plik XML, co ułatwi zarządzanie i aktualizację oczekiwanych wyników bez potrzeby ingerowania w kod źródłowy testów. To podejście znacząco usprawnia zarządzanie testami i może przyczynić się do lepszej organizacji oraz czytelności testów. Należałoby przygotować po jednym pliku dla każdego przypadku testowego. Czy istnieje lepszy sposób?</p>
<h1 id="heading-testowanie-przeciwko-snapshotom">Testowanie przeciwko Snapshotom</h1>
<h2 id="heading-co-to-jest-snapshot-testing">Co to jest Snapshot testing?</h2>
<p>Snapshot testing, czyli testowanie migawek, to technika oprogramowania, która polega na porównywaniu aktualnego stanu obiektu z wcześniej zapisanym „zrzutem”. Automatyzacja procesu zapisywania i porównywania snapshotów jest często zapewniana przez różne SDK. Ta metoda jest szczególnie popularna w testowaniu interfejsów użytkownika, gdzie zmiany są zazwyczaj subtelne, aby nie wprowadzać zamieszania i utrzymać komfort użytkowania. Interfejsy użytkownika, takie jak HTML czy XAML, ze względu na ich względną stabilność, są idealnymi kandydatami do tego rodzaju testowania.</p>
<p>Rozwój AI otwiera nowe możliwości dla technik testowania migawek, szczególnie w kontekście analizy i interpretacji obrazów. Narzędzia takie jak <a target="_blank" href="https://learn.microsoft.com/pl-pl/azure/ai-services/computer-vision/overview-image-analysis?tabs=4-0">Analiza obrazu w usługach Azure</a> mogą przyczynić się do rozwijania nowych metodologii testowania, które będą w stanie lepiej rozumieć i interpretować zmiany wizualne. To może obejmować nie tylko tradycyjne snapshoty, ale również bardziej zaawansowane formy oceny zmian w dynamicznie generowanych interfejsach czy wizualnych aspektach aplikacji. Możliwość automatycznego rozpoznawania i oceniania zmian wizualnych dzięki AI może znacząco zwiększyć efektywność i dokładność procesów testowych, zmieniając sposób, w jaki podchodzimy do zapewnienia jakości w rozwoju oprogramowania.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737322234256/41ba42a5-576a-4b1d-b372-e4f5afadf4f2.webp?height=400" alt class="image--center mx-auto" /></p>
<h2 id="heading-testowanie-generowanych-xml-ze-snapshotow-verify">Testowanie generowanych XML ze snapshotów - Verify</h2>
<p>Wystarczyło połączyć kilka elementów, aby uzyskać rozwiązanie, które jest szybkie w implementacji i proste w utrzymaniu. <a target="_blank" href="https://github.com/VerifyTests/Verify">Verify</a> to biblioteka dla platformy .Net, umożliwiająca tworzenie testów typu snapshot. Po jej wdrożeniu cały proces testowania sprowadził się do generowania struktur i wywoływania weryfikacji, na przykład w <a target="_blank" href="https://github.com/L3mur1/SSMLBuilder.Azure/blob/master/src/Tests/SSMLBuilderAzure.UnitTests/Elements/SpeakTests.cs">SpeakTests</a>:</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">Fact()</span>]
        <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">WhenElement</span>(<span class="hljs-params"></span>)</span>
        {
            <span class="hljs-comment">// Arrange</span>
            <span class="hljs-keyword">var</span> sut = Speak.InLanguage(<span class="hljs-string">"en-US"</span>)
                .WithElement(Voice
                .Named(<span class="hljs-string">"en-US-AvaMultilingualNeural"</span>)
                .WithContent(Content.Of(<span class="hljs-string">"Why are snails slow?"</span>)));

            <span class="hljs-comment">// Act</span>
            <span class="hljs-keyword">var</span> result = sut.ToString();

            <span class="hljs-comment">// Assert</span>
            <span class="hljs-keyword">await</span> Verify(result);
        }
</code></pre>
<p>W wyniku pierwszego uruchomienia testu został wygenerowany snapshot w postaci pliku .txt, który można było nagrać i sprawdzić w <a target="_blank" href="https://learn.microsoft.com/pl-pl/azure/ai-services/speech-service/speech-studio-overview">Azure Speech Studio</a>, a następnie zapisać w systemie kontroli wersji jako oczekiwany rezultat dla kolejnych wykonania. To <a target="_blank" href="https://github.com/L3mur1/SSMLBuilder.Azure/blob/master/src/Tests/SSMLBuilderAzure.UnitTests/verify_files/SpeakTests.WhenElement.verified.txt">przykład rezultatu dla testu powyżej</a>:</p>
<pre><code class="lang-csharp">﻿&lt;speak version=<span class="hljs-string">"1.0"</span> xml:lang=<span class="hljs-string">"en-US"</span> xmlns=<span class="hljs-string">"http://www.w3.org/2001/10/synthesis"</span>&gt;
  &lt;voice name=<span class="hljs-string">"en-US-AvaMultilingualNeural"</span>&gt;Why are snails slow?&lt;/voice&gt;
&lt;/speak&gt;
</code></pre>
<p>Warto dodać, że biblioteka Verify w ciekawy sposób obsługuje testy wyjątków, traktując je tak samo jak każdy inny wynik, czyli generując snapshot oczekiwanego stanu. To <a target="_blank" href="https://github.com/L3mur1/SSMLBuilder.Azure/blob/master/src/Tests/SSMLBuilderAzure.UnitTests/verify_files/SpeakTests.WhenNoSpeakElements_Throw.verified.txt">przykład rezultatu dla wyjątku</a>:</p>
<pre><code class="lang-csharp">{
  Type: SsmlBuilderException,
  Message: No speak elements.,
  StackTrace: at SSMLBuilder.Azure.Elements.Speak.ToSsml()
}
</code></pre>
<h2 id="heading-snapshot-testing-w-pracy-nad-bibliotekami-i-open-source">Snapshot testing w pracy nad bibliotekami i open source</h2>
<p>Tworzenie SDK, w formie bibliotek, opiera się na podobnych zasadach jak tworzenie interfejsów użytkownika. W tym przypadku naszym klientem jest deweloper korzystający z naszego API, a nie końcowy użytkownik. Dbamy o jego komfort, utrzymując stabilne interfejsy, których drastyczne zmiany mogłyby wprowadzić chaos i zakłócić działanie procesów opartych na naszym rozwiązaniu.</p>
<p>Pracując nad projektami open source, umożliwiamy innym wgląd w nasz kod oraz jego ewentualną modyfikację. Z jednej strony otwiera to ogromne możliwości dzięki połączeniu umiejętności programistów nie tylko z naszej firmy. Z drugiej strony, tworzymy bardziej uniwersalne rozwiązania, które mogą zaspokoić potrzeby nie tylko naszego biznesu, ale potencjalnie całego świata. Wykorzystanie naszej biblioteki w branżach takich jak medycyna czy finanse wiąże się z większą odpowiedzialnością. Wprowadzanie zmian w kodzie przez innych programistów wymaga zaufania oraz weryfikacji, aby upewnić się, że nie wprowadzą one błędów do SDK.</p>
<p>Dzięki zastosowaniu testów migawkowych, umożliwiamy sprawdzanie poprawności działania kodu bez dogłębnej znajomości domeny. Przy proponowaniu zmian, całe community zaangażowane w tworzenie open source może zobaczyć, jak zmienią się rezultaty poszczególnych wywołań oraz zachowanie nowych interfejsów. Przechowywanie snapshotów w ogólnodostępnej bazie kodu pozwala programistom, czyli użytkownikom, na dostęp do migawek, które w połączeniu z testami można traktować jako dokumentację sterowaną zachowaniem. Dodatkowo, w przypadku błędów, mogą przeszukać naszą bazę kodu w poszukiwaniu odpowiedzi na pytanie, jaki scenariusz prowadzi do wywołania konkretnego wyjątku.</p>
<h1 id="heading-kiedy-uzywac-snapshot-testing">Kiedy używać snapshot testing?</h1>
<p>Snapshot testing jest potężnym narzędziem, szczególnie wartościowym w kilku kluczowych scenariuszach:</p>
<ol>
<li><p><strong>Sprawdzanie regresji w rozwiązaniach opartych na generatywnej sztucznej inteligencji:</strong> Testy migawek umożliwiają szybkie wykrycie i dokumentowanie niepożądanych zmian oraz ocenę jakościową, ludzkim okiem, generowanych przez GPT rozwiązań.</p>
</li>
<li><p><strong>Testowanie skomplikowanych struktur danych:</strong> Idealne do zapisywania i porównywania złożonych struktur, takich jak dokumenty czy wykresy, z oczekiwanymi wynikami, co zapewnia precyzyjne sprawdzanie zgodności.</p>
</li>
<li><p><strong>Wizualna weryfikacja rezultatów:</strong> Gdy zależy nam na szczegółowym sprawdzeniu formatowania lub wyglądu interfejsów, snapshot testing pozwala na dokładne uchwycenie i porównanie stanu wizualnego elementów, z dokładnością do whitespace.</p>
</li>
<li><p><strong>Dokumentacja struktur danych:</strong> Testy migawkowe mogą służyć jako narzędzie dokumentacyjne, umożliwiające precyzyjne śledzenie i kontrolę sposobu przekazywania danych między różnymi częściami systemu. W przypadku tworzenia rozwiązań open source, jak <a target="_blank" href="https://github.com/L3mur1/SSMLBuilder.Azure">SSMLBuilder.Azure</a> dla platformy Azure, snapshoty tworzą dokumentację, która jest zarówno precyzyjna, jak i łatwa do aktualizacji.</p>
</li>
</ol>
<p>Stosowanie testów migawkowych zapewnia solidne zabezpieczenie przed niekontrolowaną regresją i daje możliwość wzrokowego zastanowienia się nad wprowadzanymi zmianami do kontraktów. Z zainteresowaniem obserwuję, jak dynamicznie rozwijająca się branża przetwarzania obrazów oraz rosnące potrzeby weryfikacji modeli AI przeciwko regresji wpłyną na rozwój tego rodzaju narzędzi.</p>
]]></content:encoded></item></channel></rss>