<?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[Sean M. Drew]]></title><description><![CDATA[Sean M. Drew]]></description><link>https://blog.seandrew.info</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1734440349746/31ae2ff7-cc83-4d00-906a-44b8cc814a8a.png</url><title>Sean M. Drew</title><link>https://blog.seandrew.info</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 13:45:23 GMT</lastBuildDate><atom:link href="https://blog.seandrew.info/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Designing a SOLID, Layered ASP.NET Core Solution for Patient Lookup]]></title><description><![CDATA[A simple patient lookup might not sound exciting, but it is a perfect chance to show how clean code and solid architecture make all the difference. This example uses ASP.NET Core and stored procedures to do just that. While the use case is straightfo...]]></description><link>https://blog.seandrew.info/designing-a-solid-layered-aspnet-core-solution-for-patient-lookup</link><guid isPermaLink="true">https://blog.seandrew.info/designing-a-solid-layered-aspnet-core-solution-for-patient-lookup</guid><category><![CDATA[C#]]></category><category><![CDATA[c#]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Wed, 30 Jul 2025 12:48:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753879626563/d2100ad4-5a82-4fa4-a608-0d903440dcb3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A simple patient lookup might not sound exciting, but it is a perfect chance to show how clean code and solid architecture make all the difference. This example uses <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core and stored procedures to do just that. While the use case is straightforward, the architecture adheres to the SOLID principles, maintains separation of concerns, and follows clean code and enterprise-level structuring. This makes the project a practical example of how even simple features benefit from strong software design.</p>
<p>Software development is not only about delivering functionality it is about delivering maintainable, scalable, and testable solutions. Even in small applications or single-feature projects like a patient lookup, poor design decisions can lead to tight coupling, duplication, and fragile code that is difficult to evolve.</p>
<p>The goal is to try and demonstrate how to take a simple use case retrieving a patient by ID from a SQL Server database and wrap it in a SOLID-compliant architecture using <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core. The goal is to illustrate how enterprise-quality design is not just for complex systems. It brings clarity, reliability, and extensibility to any codebase, regardless of size.</p>
<p>By isolating responsibilities into Models, Repositories, Services, and Web UI layers, you can ensure the system remains:</p>
<ul>
<li><p>Easy to understand and reason about</p>
</li>
<li><p>Simple to test and mock</p>
</li>
<li><p>Safe to extend or refactor</p>
</li>
<li><p>Well-suited to real-world business logic growth</p>
</li>
</ul>
<p>Even if the system starts small, this approach sets a strong foundation that avoids rewrites and future technical debt. It is not about overengineering, but about applying clean architectural principles from the beginning when they are cheapest and most effective to implement.</p>
<p><strong>Goals</strong></p>
<ul>
<li><p>Clean separation of concerns using N-Tier Architecture</p>
</li>
<li><p>Full adherence to the SOLID principles</p>
</li>
<li><p>Use of Dependency Injection, async patterns, and stored procedures</p>
</li>
<li><p>A minimal but functional Web API and Razor Page frontend</p>
</li>
<li><p>Centralized configuration and clean routing</p>
</li>
<li><p>A project structure that scales with growth</p>
</li>
</ul>
<p><strong>Recommended Solution Structure</strong><br />This solution is organized into the following projects:</p>
<p>PatientLookupSolution<br />├── PatientLookup.Models // Plain old class object<br />├── PatientLookup.Repositories // SQL logic and <a target="_blank" href="http://ADO.NET">ADO.NET</a> (SqlDataClient)<br />├── <a target="_blank" href="http://PatientLookup.Services">PatientLookup.Services</a> // Business logic<br />└── PatientLookup.Web // API and Razor UI</p>
<p>Each layer only depends on the one below it:</p>
<ul>
<li><p>Web depends on Services</p>
</li>
<li><p>Services depends on Repositories</p>
</li>
<li><p>Repositories depends on Models This architecture ensures modularity, testability, and scalability.</p>
</li>
</ul>
<p><strong>SOLID Breakdown</strong><br />Single Responsibility: Each class serves a single purpose: controller, service, repository, etc.<br />Open/Closed: You can extend behaviors (e.g., new services) without modifying existing code.<br />Liskov Substitution: Interfaces are respected; substituting mock services or test repositories is safe.<br />Interface Segregation: Interfaces are focused (e.g., IPatientService only exposes needed methods).<br />Dependency Inversion: Higher layers depend on abstractions (IPatientRepository), not implementations.</p>
<p><strong>Code Walkthrough</strong><br />Patient.cs (Model Layer)</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">namespace</span> <span class="hljs-title">PatientLookup.Models</span>
{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Patient</span>
  {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> FirstName { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-keyword">string</span>.Empty;
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> LastName { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-keyword">string</span>.Empty;
  }
}
</code></pre>
<ul>
<li><p>Just a normal class, no attributes describing infrastructure concerns or other responsibilities</p>
</li>
<li><p>No data annotations to keep the model agnostic of UI or DB layers.</p>
</li>
</ul>
<p>IPatientRepository<br />This is the part of the application responsible for talking directly to the database in a controlled and consistent way. It defines the contract for how the rest of the system can ask for patient data without needing to know how that data is actually retrieved.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IPatientRepository</span>
{
  Task&lt;Patient?&gt; GetByIdAsync(<span class="hljs-keyword">int</span> id);
}
</code></pre>
<p>Basically it tells the system: "If you give me a patientId, I will return the matching Patient object asynchronously."</p>
<p>But it does not say how it does that. The actual logic of running the stored procedure and mapping the SQL result to a model is handled in the class that implements the IPatientRepository public interface (PatientRepository).</p>
<p>IPatientRepository defines a clear boundary between the application and the data layer. It is a small piece with a big impact when it comes to keeping the code clean, testable, and future-proof.</p>
<p>PatientRepository (Repository Layer)<br />PatientRepository is the implementation of IPatientRepository. It contains the actual logic for accessing the database by calling a stored procedure to fetch patient data. It focuses only on fetching data. No business logic. No UI code. This keeps the data layer clean, testable, and easy to change without breaking the rest of the system.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PatientRepository</span> : <span class="hljs-title">IPatientRepository</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">string</span> _connectionString;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">PatientRepository</span>(<span class="hljs-params">IConfiguration config</span>)</span>
  {
    _connectionString = config.GetConnectionString(<span class="hljs-string">"DefaultConnection"</span>)
      ?? <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> InvalidOperationException(<span class="hljs-string">"Missing DefaultConnection string."</span>);
  }

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;Patient?&gt; GetByIdAsync(<span class="hljs-keyword">int</span> id)
  {
    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> conn = <span class="hljs-keyword">new</span> SqlConnection(_connectionString);
    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> cmd = <span class="hljs-keyword">new</span> SqlCommand(<span class="hljs-string">"cp_GetPatientById"</span>, conn)
    {
      CommandType = CommandType.StoredProcedure
    };
    cmd.Parameters.AddWithValue(<span class="hljs-string">"@patid"</span>, id);
    <span class="hljs-keyword">await</span> conn.OpenAsync();

    <span class="hljs-keyword">using</span> <span class="hljs-keyword">var</span> reader = <span class="hljs-keyword">await</span> cmd.ExecuteReaderAsync();
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> reader.ReadAsync())
    {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Patient
      {
        Id = reader.GetInt32(reader.GetOrdinal(<span class="hljs-string">"Id"</span>)),
        FirstName = reader.GetString(reader.GetOrdinal(<span class="hljs-string">"FirstName"</span>)),
        LastName = reader.GetString(reader.GetOrdinal(<span class="hljs-string">"LastName"</span>))
      };
    }

    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }
}
</code></pre>
<ul>
<li><p>Uses <a target="_blank" href="http://ADO.NET">ADO.NET</a> for performance and transparency.</p>
</li>
<li><p>Clean separation from business logic.</p>
</li>
<li><p>Uses a stored procedure for optimized and secure DB access.</p>
</li>
</ul>
<p>IPatientService<br />IPatientService defines the business-facing contract for how the application can retrieve patient information. It sits above the repository and lets higher layers (like the controller) request patient data without knowing anything about the data source. This abstraction makes the service layer testable, swappable, and cleanly separated from the database layer.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IPatientService</span>
{
  Task&lt;Patient?&gt; GetPatientByIdAsync(<span class="hljs-keyword">int</span> id);
}
</code></pre>
<ul>
<li><p>Acts as the middleman between the controller and the data layer, handling patient-related business logic.</p>
</li>
<li><p>Offers a simple async method "GetPatientByIdAsync" to fetch a patient by their ID.</p>
</li>
</ul>
<p>PatientService (Service Layer)<br />PatientService is the implementation of IPatientService. It calls the repository to fetch data and can apply business rules, validation, logging, or transformations before returning the result. Basically, it acts as a bridge between the controller and the repository cleanly separating concerns and centralizing any logic beyond raw data access.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PatientService</span> : <span class="hljs-title">IPatientService</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IPatientRepository _repository;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">PatientService</span>(<span class="hljs-params">IPatientRepository repository</span>)</span>
  {
    _repository = repository;
  }

  <span class="hljs-keyword">public</span> Task&lt;Patient?&gt; GetPatientByIdAsync(<span class="hljs-keyword">int</span> id)
  {
    <span class="hljs-keyword">return</span> _repository.GetByIdAsync(id);
  }
}
</code></pre>
<p>PatientController (Web API Layer)<br />PatientController is the entry point of the application. It handles incoming HTTP requests, delegates work to the service layer, and returns appropriate responses (usually JSON or a view). In this case, it returns the response to a view.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">Route(<span class="hljs-meta-string">"api/[controller]"</span>)</span>]
[<span class="hljs-meta">ApiController</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PatientController</span> : <span class="hljs-title">ControllerBase</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IPatientService _service;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">PatientController</span>(<span class="hljs-params">IPatientService service</span>)</span>
  {
    _service = service;
  }

  [<span class="hljs-meta">HttpGet(<span class="hljs-meta-string">"{id}"</span>)</span>]
  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">GetPatient</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> id</span>)</span>
  {
    <span class="hljs-keyword">var</span> patient = <span class="hljs-keyword">await</span> _service.GetPatientByIdAsync(id);
    <span class="hljs-keyword">if</span> (patient == <span class="hljs-literal">null</span>)
      <span class="hljs-keyword">return</span> NotFound();

    <span class="hljs-keyword">return</span> Ok(patient);
  }
}
</code></pre>
<ul>
<li><p>Validates incoming requests at a high level (e.g., route-level).</p>
</li>
<li><p>Delegates the actual logic to the service layer.</p>
</li>
<li><p>Returns a clean HTTP response (200 OK, 404 Not Found, etc.).</p>
</li>
<li><p>It does not contain business or data logic.</p>
</li>
<li><p>Minimal and clean controller.</p>
</li>
<li><p>Relies only on the service interface.</p>
</li>
</ul>
<p>Razor Page: PatientView.cshtml</p>
<pre><code class="lang-csharp">@page
@model PatientLookup.Web.Pages.PatientViewModel
@{
  ViewData[<span class="hljs-string">"Title"</span>] = <span class="hljs-string">"Patient Details"</span>;
}
&lt;h2&gt;Patient Details&lt;/h2&gt;
@if (Model.Patient != <span class="hljs-literal">null</span>)
{
  &lt;div&gt;
    &lt;strong&gt;ID:&lt;/strong&gt; @Model.Patient.Id&lt;br /&gt;
    &lt;strong&gt;Name:&lt;/strong&gt; @Model.Patient.FirstName @Model.Patient.LastName
        &lt;/div&gt;
}
<span class="hljs-keyword">else</span>
{
  &lt;div&gt;Patient not found.&lt;/div&gt;
}
</code></pre>
<p>Code-Behind: PatientView.cshtml</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">PatientViewModel</span> : <span class="hljs-title">PageModel</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IPatientService _service;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">PatientViewModel</span>(<span class="hljs-params">IPatientService service</span>)</span>
  {
    _service = service;
  }

  [<span class="hljs-meta">BindProperty(SupportsGet = true)</span>]
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

  <span class="hljs-keyword">public</span> Patient? Patient { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">OnGetAsync</span>(<span class="hljs-params"></span>)</span>
  {
    Patient = <span class="hljs-keyword">await</span> _service.GetPatientByIdAsync(Id);
    <span class="hljs-keyword">return</span> Patient == <span class="hljs-literal">null</span> ? NotFound() : Page();
  }
}
</code></pre>
<ul>
<li><p>Clean and simple frontend view.</p>
</li>
<li><p>Demonstrates reuse of business logic via dependency injection in Razor Pages.</p>
</li>
</ul>
<p>Program.cs<br />Program.cs is the glue that connects and configures all the architectural layers at startup. It is the application entry point in <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core and configures the web host, registers services for dependency injection, defines how the application will run, etc.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> Microsoft.AspNetCore.Builder;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Configuration;
<span class="hljs-keyword">using</span> Microsoft.Extensions.DependencyInjection;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Hosting;
<span class="hljs-keyword">using</span> PatientLookup.Repositories;
<span class="hljs-keyword">using</span> PatientLookup.Services;
<span class="hljs-keyword">using</span> System.Data;
<span class="hljs-keyword">using</span> System.Data.SqlClient;

<span class="hljs-keyword">var</span> builder = WebApplication.CreateBuilder(args);

<span class="hljs-comment">// Load configuration</span>
builder.Configuration.AddJsonFile(<span class="hljs-string">"appsettings.json"</span>, optional: <span class="hljs-literal">false</span>, reloadOnChange: <span class="hljs-literal">true</span>);

<span class="hljs-comment">// Register controllers and framework services</span>
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

<span class="hljs-comment">// Register custom services and repositories</span>
builder.Services.AddScoped&lt;IPatientRepository, PatientRepository&gt;();
builder.Services.AddScoped&lt;IPatientService, PatientService&gt;();

<span class="hljs-comment">// Register SQL connection for dependency injection</span>
builder.Services.AddScoped&lt;IDbConnection&gt;(sp =&gt;
  <span class="hljs-keyword">new</span> SqlConnection(builder.Configuration.GetConnectionString(<span class="hljs-string">"DefaultConnection"</span>)));

<span class="hljs-keyword">var</span> app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthorization();

app.MapControllers();
app.Run();
</code></pre>
<p>appsettings.json (environment based configuration)</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"ConnectionStrings"</span>: {
    <span class="hljs-attr">"DefaultConnection"</span>: <span class="hljs-string">"Server=SQLServer;Database=YourDb;Trusted_Connection=True;"</span>
  }
}
</code></pre>
<p><strong>Best Practice Highlights</strong></p>
<ul>
<li><p>Use Stored Procedures: More secure, better for performance.</p>
</li>
<li><p>Separation of Concerns: Data, business, and presentation logic are isolated.</p>
</li>
<li><p>Thin Controllers: Controllers delegate logic to services.</p>
</li>
<li><p>Interface-Driven Design: Easily testable and extensible.</p>
</li>
<li><p>Async Everywhere: Improves scalability.</p>
</li>
</ul>
<p>How all the Layers Work Together<br />This architecture separates responsibilities across four key layers, each with a clear purpose:</p>
<ol>
<li><p>Controller Layer<br /> Responsibility: Handles HTTP requests and returns responses.<br /> Example: PatientController<br /> Accepts GET /api/patient/123<br /> Calls IPatientService.GetPatientAsync(123)<br /> Returns 200 OK with the patient object or 404 Not Found</p>
</li>
<li><p>Service Layer<br /> Responsibility: Contains business logic and orchestrates data access.<br /> Example: PatientService<br /> Validates input (e.g., patientId &gt; 0)<br /> Calls repository: IPatientRepository.GetPatientByIdAsync()<br /> Applies any business rules before returning data</p>
</li>
<li><p>Repository Layer<br /> Responsibility: Communicates with the database (via stored procedures or queries).<br /> Example: PatientRepository<br /> Executes cp_APIGetPatientById<br /> Maps SqlDataReader result to a Patient model<br /> Returns the data to the service layer</p>
</li>
<li><p>Model Layer<br /> Responsibility: Defines the shape of your data objects.<br /> Example: Patient class<br /> Represents patient data across layers<br /> Keeps your code strongly typed and structured</p>
</li>
</ol>
<p>Separation of Concerns: Each layer does one thing and does it well<br />Testability: You can mock or stub any layer during unit testing<br />Maintainability: Changes in one layer (e.g., database schema) do not break the rest<br />Scalability: Easily extend logic (e.g., add caching or logging) without disrupting the design</p>
<p><strong>Conclusion</strong><br />This example illustrates how clean, scalable architecture is not limited to large enterprise projects. By applying the SOLID principles, structuring with layered boundaries, and embracing clean code practices, you create software that is:</p>
<ul>
<li><p>Easy to maintain</p>
</li>
<li><p>Easy to extend</p>
</li>
<li><p>Easier to onboard new developers</p>
</li>
<li><p>Ready for scaling into production</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Refactoring Repetitive Model Validation in ASP.NET Core]]></title><description><![CDATA[In one of my ASP.NET Core APIs, I was working with [FromBody] models that had a number of required fields, some were nullable integers, others were strings that needed to be non-empty. Initially, I handled validation directly in the controller action...]]></description><link>https://blog.seandrew.info/refactoring-repetitive-model-validation-in-aspnet-core</link><guid isPermaLink="true">https://blog.seandrew.info/refactoring-repetitive-model-validation-in-aspnet-core</guid><category><![CDATA[C#]]></category><category><![CDATA[API basics ]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Wed, 16 Jul 2025 14:58:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752677776386/b239ea4d-be58-4c98-99e5-b742efb05663.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In one of my <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core APIs, I was working with [FromBody] models that had a number of required fields, some were nullable integers, others were strings that needed to be non-empty. Initially, I handled validation directly in the controller action with a series of repetitive “if” statements like this:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (!model.patienId!.HasValue || (<span class="hljs-keyword">int</span>)model.patienId! == <span class="hljs-number">0</span>)
{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">"patienId is a required parameter"</span>, StatusCode = <span class="hljs-number">400</span> }; }

<span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrEmpty(model.ndcnumber))
{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">"ndcnumber is a required parameter"</span>, StatusCode = <span class="hljs-number">400</span> }; }

<span class="hljs-keyword">if</span> (!model.qty!.HasValue || (<span class="hljs-keyword">int</span>)model.qty! &lt;= <span class="hljs-number">0</span>) 
{ <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">"qty is a required parameter"</span>, StatusCode = <span class="hljs-number">400</span> }; }

<span class="hljs-comment">// ... and so on for every field</span>
</code></pre>
<p>This approach worked well for what I was doing at the time, but it became messy very quickly, especially as I added more fields and repeated the same pattern in multiple actions and across different controllers. It was hard to read, error-prone to update, and encouraged copy/paste over clean reuse.</p>
<p>To clean this up, I decided to refactor the common validation checks into a dedicated "ValidationHelper" class. This utility would let me validate nullable value types and strings in a reusable and centralized way.</p>
<p>In this post, I show a simple refactor I made to clean up these types of repetitive validation "if" statements in my controller actions by introducing a lightweight ValidationHelper. The result is clearer code, consistent error handling, and much better maintainability.</p>
<p><strong>Refactoring with a Validation Helper</strong><br />Basically, a helper class is a fancy way to describe a separate CS file where I put reusable code like small utility functions that I can call from my controllers (or anywhere else). It keeps things clean and avoids repeating the same logic in every method. So, instead of cluttering my controllers with repetitive checks (like verifying required fields), I just put those checks in a helper class and call them as needed. It makes the code easier to read and maintain.</p>
<p>This helper lives in its own file under the "/Helpers/ValidationHelper.cs" folder, and I keep it in the shared "MyAPI.Helpers" namespace.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// simple helper method for validating required model fields in ASP.NET Core controllers.</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ValidationHelper</span>
{
  <span class="hljs-comment">// Validate a nullable value type is not null and not its default value (e.g., 0 for int, false for bool).</span>
  <span class="hljs-comment">// &lt;typeparam name="T"&gt;A struct type (e.g., int, bool, DateTime).&lt;/typeparam&gt;</span>
  <span class="hljs-comment">// &lt;param name="value"&gt;The value to validate.&lt;/param&gt;</span>
  <span class="hljs-comment">// &lt;param name="name"&gt;The name of the parameter (used in the error message).&lt;/param&gt;</span>
  <span class="hljs-comment">// Returns ContentResult with a 400 status if the value is missing or default; otherwise, null.</span>
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> ContentResult? Required&lt;T&gt;(T? <span class="hljs-keyword">value</span>, <span class="hljs-keyword">string</span> name) <span class="hljs-keyword">where</span> T : <span class="hljs-keyword">struct</span>
  {
    <span class="hljs-comment">// Check if the value is null or equals the default value for its type (e.g., 0 for int).</span>
    <span class="hljs-keyword">if</span> (!<span class="hljs-keyword">value</span>.HasValue || EqualityComparer&lt;T&gt;.Default.Equals(<span class="hljs-keyword">value</span>.Value, <span class="hljs-keyword">default</span>))
    {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult
      {
        Content = <span class="hljs-string">$"<span class="hljs-subst">{name}</span> is a required parameter"</span>,
        StatusCode = <span class="hljs-number">400</span>
      };
    }

    <span class="hljs-comment">// If valid, return null to indicate no validation error.</span>
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-comment">// Validate a string is not null, empty, or whitespace.</span>
  <span class="hljs-comment">// &lt;param name="value"&gt;The string value to validate.&lt;/param&gt;</span>
  <span class="hljs-comment">// &lt;param name="name"&gt;The name of the parameter (used in the error message).&lt;/param&gt;</span>
  <span class="hljs-comment">// Returns ContentResult with a 400 status if the string is null or empty; otherwise, null.</span>
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> ContentResult? Required(<span class="hljs-keyword">string</span>? <span class="hljs-keyword">value</span>, <span class="hljs-keyword">string</span> name)
  {
    <span class="hljs-comment">// Check if the string is null, empty, or whitespace-only.</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrWhiteSpace(<span class="hljs-keyword">value</span>))
    {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult
      {
        Content = <span class="hljs-string">$"<span class="hljs-subst">{name}</span> is a required parameter"</span>,
        StatusCode = <span class="hljs-number">400</span>
      };
    }

    <span class="hljs-comment">// If valid, return null to indicate no validation error.</span>
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }
}
</code></pre>
<p><strong>A Sample of My Model</strong></p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MySampleModel</span>
{
  [<span class="hljs-meta">Required</span>] <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span>? patientId { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  [<span class="hljs-meta">Required</span>] <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? ndcnumber { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-keyword">string</span>.Empty;
  [<span class="hljs-meta">Required</span>] <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span>? qty { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p><strong>Sample Usage in my Controller</strong><br />Now, instead of repeating lots of "if" validation logic in every action I simply do something like this:</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">ApiController</span>]
[<span class="hljs-meta">Route(<span class="hljs-meta-string">"[controller]"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">MyController</span> : <span class="hljs-title">ControllerBase</span>
{
  <span class="hljs-keyword">private</span> ContentResult? _validationResult;

  [<span class="hljs-meta">HttpPost(<span class="hljs-meta-string">"DoSomethingEndpoint"</span>)</span>]
  <span class="hljs-function"><span class="hljs-keyword">public</span> IActionResult <span class="hljs-title">DoSomethingEndpoint</span>(<span class="hljs-params">[FromBody] MySampleModel model</span>)</span>
  {
    <span class="hljs-comment">// Use ValidationHelper to check required fields</span>
    <span class="hljs-keyword">if</span> ((_validationResult = ValidationHelper.Required(model.patientId, <span class="hljs-string">"patientId"</span>)) != <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> _validationResult;
    <span class="hljs-keyword">if</span> ((_validationResult = ValidationHelper.Required(model.ndcnumber, <span class="hljs-string">"ndcnumber"</span>)) != <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> _validationResult;
    <span class="hljs-keyword">if</span> ((_validationResult = ValidationHelper.Required(model.qty, <span class="hljs-string">"qty"</span>)) != <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> _validationResult;

    <span class="hljs-comment">// if all fields are valid, proceed with processing</span>
    <span class="hljs-keyword">return</span> Ok(<span class="hljs-string">"Request accepted"</span>);
  }
}
</code></pre>
<p><strong>Controller-Wide Result Field</strong><br />To avoid declaring the same "ContentResult? result;" variable in every action, I made it a private field on the controller, like this:<br /><code>private ContentResult? _validationResult;</code></p>
<p>Then I reused it across all of my methods.<br /><code>if ((_validationResult = ValidationHelper.Required(model.qty, "qty")) != null) return _validationResult;</code><br />Because <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core creates a new instance of the controller per request, this pattern is safe and avoids unnecessary duplication while keeping the logic tight and readable.</p>
<p><strong>Why This Is Better</strong><br />Switching to a helper-based approach gave me several benefits:<br />• Cleaner, reusable validation logic. the core logic lives in one place and is reused everywhere<br />• Consistent error formatting, validation lines are short, clear, and consistent<br />• Easier maintenance. Changes to the validation rules only require updates in one spot. Easy to extend and apply to other models.<br />• Cleaner controller actions. No external validation libraries required</p>
<p><strong>Conclusion</strong><br />This refactor started as a way to clean up clutter in a single action, but it quickly became a general pattern I now apply across my API projects.</p>
<p>Also, for more complex validation rules, like conditional logic (e.g., field B is required if field A is X) I can simply extend this pattern or supplement it with FluentValidation. If you're writing the same validation statements over and over, give a helper like this a try. It might be all you need to make your codebase more elegant and maintainable.</p>
]]></content:encoded></item><item><title><![CDATA[Transitioning to JSON: Supporting Both Body and Query Parameters in ASP.NET Core]]></title><description><![CDATA[Recently, I was tasked with updating older POST endpoints to support [FromBody] input instead of the traditional [FromQuery] model. As part of this transition, I needed to support both [FromQuery] and [FromBody] inputs for backward compatibility unti...]]></description><link>https://blog.seandrew.info/transitioning-to-json-supporting-both-body-and-query-parameters-in-aspnet-core</link><guid isPermaLink="true">https://blog.seandrew.info/transitioning-to-json-supporting-both-body-and-query-parameters-in-aspnet-core</guid><category><![CDATA[C#]]></category><category><![CDATA[API basics ]]></category><category><![CDATA[api]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Mon, 14 Jul 2025 12:44:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752497037137/cdc8c900-aa1c-4d3e-a8cf-9b2b06f826e1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I was tasked with updating older POST endpoints to support [FromBody] input instead of the traditional [FromQuery] model. As part of this transition, I needed to support both [FromQuery] and [FromBody] inputs for backward compatibility until all clients can be updated. This approach allows existing clients to continue functioning without interruption, while new clients benefit from cleaner, structured JSON.</p>
<p>While this pattern is helpful for POST or PUT endpoints where the client is sending data, it is not intended for GET requests. In GET requests the input should remain as route or query parameters and not the request body.</p>
<p>Sounds simple, right? But in practice, <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core’s model binding and validation pipeline is not designed to easily accept both [FromBody] and [FromQuery] inputs for the same model. You typically need to choose one source of input (not both).</p>
<p>In this write-up I go through a (relatively) reusable way to support both input models without introducing unnecessary complexity or breaking existing client functionality.</p>
<p><strong>Objective</strong><br />Allow my <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web API POST/PUT endpoints to accept parameters either through:<br />• JSON body (using [FromBody])<br />• Query string (using [FromQuery])<br />The logic is:<br />• If the body exists and is valid then use it instead of query parameters.<br />• If the body is null or invalid then look at query parameters.<br />• If the query parameters are null or invalid then re-check body as fallback.<br />• If neither exist or are invalid then return a validation error.</p>
<p><strong>The Problem</strong><br />• If I decorate my action parameters with just [FromBody], then clients must send JSON. If they do not, they will see errors like “Unsupported Media Type” or model validation failures because the body is missing. I am doing this to help with migration so existing clients do not have to change right away and to support a smooth transition period.<br />• Mixing [FromBody] and [FromQuery] on the same action parameters does not work particularly well and could throw unexpected validation failures.<br />• I need to move to a JSON body input but fallback gracefully to query parameters if the body is missing or invalid, especially for existing clients.<br />• I want validation to “just work” with either input method.</p>
<p><strong>My Solution is an Extension Method That Takes Care of Both Input Types</strong><br />Here is how I approached it:</p>
<p>• I wrote a helper method that first tries to read and validate my model from the JSON body. A helper method is a fancy way of saying "code" (method) that I have in a in a class file. This helps to simplify and centralize logic that I might otherwise duplicate across multiple places.</p>
<p>• If that does not work (maybe because the client sent no body or the JSON is invalid) then it then tries to build and validate the model from query parameters instead.</p>
<p>• The plan is to end up with one valid model object but it is also possible that neither input works.</p>
<p>• I call this helper from my controller action. If it returns a valid model, preferably from the JSON body (which is the goal here) then I continue with my business logic. If it doesn’t then I return a validation error to the caller.</p>
<p><strong>What Does That Look Like?</strong><br />I added a new class file named HttpRequestExtensions.cs to my project.</p>
<p>The file contains an extension method for my <a target="_blank" href="http://ASP.NET">ASP.NET</a> controllers that helps seamlessly retrieve and validate a model from either the JSON request body ([FromBody]) or the URL query parameters ([FromQuery]). In a nut shell:<br />• Read and parse the JSON body first, validating the model if present.<br />• If the body is missing or invalid, it then attempts to build and validate the model from the query string parameters.<br />• It returns the first valid model it finds, or null if neither source is valid.</p>
<p>This simplifies my controller code by centralizing the logic for supporting both input styles, making migration smoother and keeping the endpoint implementation clean and consistent.</p>
<p>Here is what the HttpRequestExtensions.cs file extension method looks like.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// This static class provides an extension method for Controller to support input binding</span>
<span class="hljs-comment">// from either JSON body ([FromBody]) or query string ([FromQuery]).</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HttpRequestExtensions</span>
{
  <span class="hljs-comment">// Generic method to try binding a model of type T from the body or query.</span>
  <span class="hljs-comment">// T must be a class with a parameterless constructor.</span>
  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> <span class="hljs-title">Task</span>&lt;<span class="hljs-title">T</span>?&gt; <span class="hljs-title">TryGetModelFromBodyOrQueryAsync</span>&lt;<span class="hljs-title">T</span>&gt;(<span class="hljs-params"><span class="hljs-keyword">this</span> Controller controller</span>) <span class="hljs-keyword">where</span> T : class, <span class="hljs-title">new</span>(<span class="hljs-params"></span>)</span>
  {
    <span class="hljs-keyword">var</span> request = controller.HttpContext.Request;

    <span class="hljs-comment">// --- STEP 1: Try to bind from JSON body if content exists and is of type application/json ---</span>
    <span class="hljs-keyword">if</span> (request.ContentLength &gt; <span class="hljs-number">0</span> &amp;&amp; 
        request.ContentType?.Contains(<span class="hljs-string">"application/json"</span>, StringComparison.OrdinalIgnoreCase) == <span class="hljs-literal">true</span>)
    {
      <span class="hljs-keyword">try</span>
      {
        <span class="hljs-comment">// Attempt to deserialize the JSON body into the model</span>
        <span class="hljs-keyword">var</span> bodyModel = <span class="hljs-keyword">await</span> request.ReadFromJsonAsync&lt;T&gt;();

        <span class="hljs-comment">// If deserialization is successful and model passes validation, return it</span>
        <span class="hljs-keyword">if</span> (bodyModel != <span class="hljs-literal">null</span> &amp;&amp; controller.TryValidateModel(bodyModel)) 
          <span class="hljs-keyword">return</span> bodyModel;
      }
      <span class="hljs-keyword">catch</span>
      {
        <span class="hljs-comment">// Ignore JSON parsing errors and fall through to try query string</span>
      }
    }

    <span class="hljs-comment">// --- STEP 2: Fallback to building the model from query string parameters ---</span>
    <span class="hljs-keyword">var</span> queryModel = <span class="hljs-keyword">new</span> T(); <span class="hljs-comment">// Create a new instance of T</span>
    <span class="hljs-keyword">var</span> props = <span class="hljs-keyword">typeof</span>(T).GetProperties(); <span class="hljs-comment">// Reflectively get all public properties</span>

    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> prop <span class="hljs-keyword">in</span> props)
    {
      <span class="hljs-keyword">var</span> key = prop.Name;

      <span class="hljs-comment">// Skip if the query string does not contain the key</span>
      <span class="hljs-keyword">if</span> (!request.Query.ContainsKey(key)) <span class="hljs-keyword">continue</span>;

      <span class="hljs-keyword">var</span> <span class="hljs-keyword">value</span> = request.Query[key].ToString();

      <span class="hljs-comment">// Skip if value is empty or whitespace</span>
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrWhiteSpace(<span class="hljs-keyword">value</span>)) <span class="hljs-keyword">continue</span>;

      <span class="hljs-keyword">try</span>
      {
        <span class="hljs-comment">// Determine the correct type, handling nullable types</span>
        <span class="hljs-keyword">var</span> targetType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;

        <span class="hljs-comment">// Convert the string query value to the correct property type</span>
        <span class="hljs-keyword">var</span> convertedValue = Convert.ChangeType(<span class="hljs-keyword">value</span>, targetType, CultureInfo.InvariantCulture);

        <span class="hljs-comment">// Assign the converted value to the property</span>
        prop.SetValue(queryModel, convertedValue);
      }
      <span class="hljs-keyword">catch</span>
      {
        <span class="hljs-comment">// Ignore conversion errors (e.g., invalid format); leave default value</span>
      }
    }

    <span class="hljs-comment">// Return the query-bound model if it passes validation; otherwise return null</span>
    <span class="hljs-keyword">return</span> controller.TryValidateModel(queryModel) ? queryModel : <span class="hljs-literal">null</span>;
  }
}
</code></pre>
<p><strong>How I use it in my controller</strong><br />I replaced the existing model-binding code in my controller with this for each of the actions:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> model = <span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.TryGetModelFromBodyOrQueryAsync&lt;MyModelType&gt;();
<span class="hljs-keyword">if</span> (model == <span class="hljs-literal">null</span>)
    <span class="hljs-keyword">return</span> ValidationProblem(ModelState);

<span class="hljs-comment">// Proceed with my normal business logic using the valid model</span>
</code></pre>
<p><strong>Why Is This Awesome?</strong><br />• Clean &amp; simple controller actions with no messy manual parameter parsing.<br />• Supports both old clients (query string) and new clients (JSON body).<br />• Graceful fallback logic. JSON body is preferred but will fall back to query parameters (or error out).<br />• It is reusable and generic. It works with any of my public property model classes that are defined in my Model.</p>
<p><strong>Is This Method a Good Approach?</strong><br /><strong>It depends.</strong> Supporting both [FromBody] and [FromQuery] is not ideal for long-term API design, but it serves as a practical and effective short-term solution when transitioning existing endpoints from query parameters to JSON body input without breaking existing clients<br />• Have clients relying on query parameters.<br />• Want a clean and reusable way to handle dual input methods (JSON and parameters).<br />• Want validation to work seamlessly for either input method (JSON and parameters).<br />• Want to avoid complex conditional code scattered in my controllers.</p>
<p><strong>Pros of This Approach</strong><br />• Backwards compatibility: Existing clients keep working while new clients use JSON.<br />• Future-proof: Existing clients can switch to JSON body without changing server logic.<br />• Fewer Breaking Changes: Avoids forcing clients to rewrite request logic.<br />• Clean controller code: Single line to get your model, no manual parameter parsing.<br />• Generic and reusable: Can be used with any defined model class.<br />• Graceful fallback: Prefers JSON body but falls back to query parameters if body is missing or invalid.</p>
<p><strong>Cons of This Approach</strong><br />• Increased complexity: The API endpoint is no longer truly RESTful in a pure sense because it supports two very different input mechanisms simultaneously.<br />• Not Idiomatic: <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core expects one clear model-binding path (e.g., [FromBody] or [FromQuery]). Supporting both circumvents the framework’s intended design.<br />• Potential Maintenance Burden: basically supporting two versions of input in one endpoint.<br />• Validation Complexity: Now have to validate two sources and resolve conflicts cleanly.<br />• Performance overhead: Minor overhead in parsing and validation twice per request.</p>
<p><strong>Is There a Better Way?</strong><br />Of course, there always is.<br />• Separate endpoints: Keep old endpoints accepting query parameters for legacy, and new ones accepting JSON body, rather than mixing both in one action. Create one endpoint that supports query parameters (/v1/endpoint) and another that only accepts a JSON body (/v2/endpoint). This is clean, versioned, and explicit.<br />• Custom model binders: Implement a custom model binder that can elegantly handle both query and body input, but this requires more advanced <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core expertise.<br />• API Gateway or Middleware: Use an API gateway or middleware that transforms query parameters into JSON bodies before the request reaches your controller.<br />• Deprecate Gradually: Add support for JSON body now, log usage, and phase out query parameter support over time and eventually simplify the codebase to expect only one model-binding source (JSON).</p>
<p><strong>Is Supporting Both JSON Body and Query Parameters a NoNo?</strong><br />• Although not a strict “NoNo,” it is generally discouraged for clarity and maintainability reasons.<br />• APIs should ideally be explicit and consistent in how they accept input, mixing sources could potentially confuse both clients and maintainers.<br />• During transitions or migrations supporting both input styles is a reasonable and often necessary compromise.<br />• Keep the logic simple and well documented if supporting both input methods.<br />• Ultimately, the best practice is to have a clear contract (either query parameters or JSON body).</p>
<p><strong>Why I Chose to Do This</strong></p>
<ol>
<li><p>Transition Period During API Modernization I am modernizing older API endpoints that originally only accepted query parameters, but now need to support more expressive JSON payloads. Instead of requiring all clients to switch immediately, I am allowing both input methods temporarily to maintain backward compatibility. This approach ensures that existing clients keep working without forcing major changes on them.</p>
</li>
<li><p>Convenience for Testing and Prototyping When I am testing endpoints in Postman it is easier to pass a few parameters through the URL for quick tests. This is especially helpful during development when the payload is small or simple.</p>
</li>
</ol>
<p><strong>When to Avoid</strong><br />• Building a new API from scratch.<br />• For clean consistent POST/PUT API contracts.<br />• Prioritize long-term maintainability over short-term flexibility.</p>
<p><strong>Summary</strong></p>
<ol>
<li><p>How easy is it to implement? • This approach takes a bit of setup since it uses a helper method. • Other options might be trickier, like writing a custom model binder.</p>
</li>
<li><p>How good is it for backwards compatibility? • This is one of the biggest strengths of this method—it lets old clients keep working without changes. • Other approaches might need separate versioned endpoints.</p>
</li>
<li><p>How clear and maintainable is the code? • Mixing query and body input adds a little complexity. • Alternatives that use one clear input model are easier to read and maintain.</p>
</li>
<li><p>Does validation still work well? • Yep. FluentValidation works fine with this method. • Same goes for most alternatives.</p>
</li>
<li><p>Is this how <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core is “supposed” to be used? • Not exactly. It bends the usual model binding rules a bit. • Alternatives tend to stick closer to how the framework was designed to work.</p>
</li>
</ol>
<p><strong>Final Thoughts</strong><br />This approach helps me keep the API backwards compatible without cluttering up the controller logic. It is a practical middle ground while I migrate endpoints, especially since I still need to support some legacy clients for now.</p>
<p>Once I am ready to drop query parameters entirely, I can just update the action signatures and remove the fallback helper with no major refactoring needed.</p>
<p><strong>Bottom Line</strong><br />• In the short term, I am okay supporting both [FromQuery] and [FromBody]as I am doing this intentionally with clear fallback rules and proper validation.<br />• For the long term, the plan is to eventually fully migrate to [FromBody] because it is cleaner and aligns better with modern, RESTful API design. Eventually I will deprecate [FromQuery] for POST and PUT requests.<br />• As a transition strategy my goal is to move each existing endpoint to JSON body input when the time is right.</p>
]]></content:encoded></item><item><title><![CDATA[SQL Server Stored Procedure Design for Flexible Record Lookup]]></title><description><![CDATA[When working with data I often run into situations where the available information varies. Sometimes I get a clean patientid, which makes the lookup easy. Other times, all I have is a date of birth and ZIP code or even just an invoice number from bil...]]></description><link>https://blog.seandrew.info/sql-server-stored-procedure-design-for-flexible-record-lookup</link><guid isPermaLink="true">https://blog.seandrew.info/sql-server-stored-procedure-design-for-flexible-record-lookup</guid><category><![CDATA[T-SQL]]></category><category><![CDATA[SQL]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Thu, 10 Jul 2025 20:56:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1752180953058/de7e1e65-8b4f-4699-91c9-918ac3c2bbfa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When working with data I often run into situations where the available information varies. Sometimes I get a clean patientid, which makes the lookup easy. Other times, all I have is a date of birth and ZIP code or even just an invoice number from billing.</p>
<p>Instead of creating a separate stored procedure for each case or writing messy dynamic SQL, I prefer a cleaner approach. I design one flexible stored procedure that can handle all of these lookup paths while keeping my logic organized which makes the code easier to maintain and helps my system adapt to whatever input it receives.</p>
<p><strong>What This Procedure Supports</strong></p>
<ul>
<li><p>Lookups by @patientid</p>
</li>
<li><p>Lookups by <a target="_blank" href="https://dev.to/dob">@dob</a> and <a target="_blank" href="https://dev.to/zip">@zip</a></p>
</li>
<li><p>Lookups by @invoice_no</p>
</li>
<li><p>Optional parameters with fallback matching</p>
</li>
<li><p>Clean modular structure using Common Table Expressions (CTEs)</p>
</li>
<li><p>Output in structured JSON, ideal for API responses</p>
</li>
</ul>
<p><strong>Why Flexible Lookup Matters</strong><br />I rarely get the same kind of data twice. One part of the system might send me a patientid while another might only have a date of birth and ZIP code or just an invoice number.</p>
<p>Here is how it usually breaks down:</p>
<ul>
<li><p>Scenario: API call with internal ID</p>
</li>
<li><p>Inputs I Get: @patientid</p>
</li>
<li><p>Scenario: Registration</p>
</li>
<li><p>Inputs I Get: <a target="_blank" href="https://dev.to/dob">@dob</a>, <a target="_blank" href="https://dev.to/zip">@zip</a></p>
</li>
<li><p>Scenario: Billing or payment inquiry</p>
</li>
<li><p>Inputs I Get: @invoice_no</p>
</li>
</ul>
<p>That kind of variety means I need a lookup approach that can adapt. By building in flexible matching logic, I avoid writing separate stored procedures for each case which helps to keep things consistent, avoid duplication, and help me handle more situations with less code with only one stored procedure instead of multiple.</p>
<p><strong>How It Works</strong><br />The stored procedure follows a clear order of evaluation:</p>
<ul>
<li><p>Scenario: @patientid provided</p>
</li>
<li><p>What Happens: Direct match</p>
</li>
<li><p>Scenario: <a target="_blank" href="https://dev.to/dob">@dob</a> + <a target="_blank" href="https://dev.to/zip">@zip</a> provided</p>
</li>
<li><p>What Happens: Match via demographics</p>
</li>
<li><p>Scenario: @invoice_no provided</p>
</li>
<li><p>What Happens: Match via invoice number</p>
</li>
<li><p>Scenario: Multiple match types provided</p>
</li>
<li><p>What Happens: Uses first match in priority order</p>
</li>
<li><p>Scenario: No match</p>
</li>
<li><p>What Happens: Returns empty JSON</p>
</li>
<li><p>Scenario: Any error</p>
</li>
<li><p>What Happens: Returns structured JSON error</p>
</li>
</ul>
<p><strong>Why Use CTEs?</strong><br />Common Table Expressions (CTEs) help keep the matching logic clean, modular, and easy to extend. Each match strategy goes into its own named block:</p>
<ul>
<li><p>Each strategy is easy to isolate and debug</p>
</li>
<li><p>Logic is easier to read and debug</p>
</li>
<li><p>Maintenance is simpler when business rules change</p>
</li>
<li><p>Match types can be reordered or extended without affecting the others</p>
</li>
</ul>
<p><strong>Stored Procedure Walkthrough</strong><br />This is a simplified version of the procedure that covers all three match types:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">use</span> [thedb]
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">drop</span> <span class="hljs-keyword">procedure</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> GetPatientSumInfo
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">set</span> ansi_nulls <span class="hljs-keyword">on</span>
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">set</span> quoted_identifier <span class="hljs-keyword">on</span>
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">create</span> <span class="hljs-keyword">procedure</span> GetPatientSumInfo
  @patientid <span class="hljs-built_in">int</span> = <span class="hljs-literal">null</span>,
  @dob <span class="hljs-built_in">date</span> = <span class="hljs-literal">null</span>,
  @zip <span class="hljs-built_in">varchar</span>(<span class="hljs-number">20</span>) = <span class="hljs-literal">null</span>,
  @invnum <span class="hljs-built_in">varchar</span>(<span class="hljs-number">50</span>) = <span class="hljs-literal">null</span>
<span class="hljs-keyword">as</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">set</span> nocount <span class="hljs-keyword">on</span>;

  <span class="hljs-keyword">begin</span> try

    ;<span class="hljs-keyword">with</span> patidmatch <span class="hljs-keyword">as</span> (
      <span class="hljs-keyword">select</span> patientid
      <span class="hljs-keyword">from</span> patinfo
      <span class="hljs-keyword">where</span> patientid = @patientid
    ),
    demographicmatch <span class="hljs-keyword">as</span> (
      <span class="hljs-keyword">select</span> p.patientid
      <span class="hljs-keyword">from</span> patinfo <span class="hljs-keyword">as</span> p
      <span class="hljs-keyword">join</span> patinfo_addr <span class="hljs-keyword">as</span> pa <span class="hljs-keyword">on</span> p.patientid = pa.patientid
      <span class="hljs-keyword">where</span>
        @patientid <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">and</span>
        @dob <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">and</span>
        @zip <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">and</span>
        p.dob = @dob <span class="hljs-keyword">and</span>
        pa.zip = @zip
    ),
    invoicematch <span class="hljs-keyword">as</span> (
      <span class="hljs-keyword">select</span> <span class="hljs-keyword">distinct</span> p.patientid
      <span class="hljs-keyword">from</span> orderinfo <span class="hljs-keyword">as</span> o
      <span class="hljs-keyword">join</span> patinfo <span class="hljs-keyword">as</span> p <span class="hljs-keyword">on</span> o.patientid = p.patientid
      <span class="hljs-keyword">where</span>
        @patientid <span class="hljs-keyword">is</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">and</span>
        @invnum <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">and</span>
        o.invnum = @invnum
    ),
    allmatches <span class="hljs-keyword">as</span> (
      <span class="hljs-keyword">select</span> patientid <span class="hljs-keyword">from</span> patidmatch
      <span class="hljs-keyword">union</span>
      <span class="hljs-keyword">select</span> patientid <span class="hljs-keyword">from</span> demographicmatch
      <span class="hljs-keyword">union</span>
      <span class="hljs-keyword">select</span> patientid <span class="hljs-keyword">from</span> invoicematch
    ),
    matchedpatient <span class="hljs-keyword">as</span> (
      <span class="hljs-keyword">select</span> top <span class="hljs-number">1</span> patientid
      <span class="hljs-keyword">from</span> allmatches
      <span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> patientid
    )
    <span class="hljs-keyword">select</span>
      p.patientid,
      p.first_name,
      p.last_name,
      p.dob,
      (
        <span class="hljs-keyword">select</span>
          pa.addr_id,
          pa.street,
          pa.city,
          pa.state,
          pa.zip
        <span class="hljs-keyword">from</span> patinfo_addr <span class="hljs-keyword">as</span> pa
        <span class="hljs-keyword">where</span> pa.patientid = p.patientid
        <span class="hljs-keyword">for</span> <span class="hljs-keyword">json</span> <span class="hljs-keyword">path</span>
      ) <span class="hljs-keyword">as</span> address
    <span class="hljs-keyword">from</span> matchedpatient <span class="hljs-keyword">as</span> m
    <span class="hljs-keyword">join</span> patinfo p <span class="hljs-keyword">on</span> p.patientid = m.patientid
    <span class="hljs-keyword">for</span> <span class="hljs-keyword">json</span> <span class="hljs-keyword">path</span>, without_array_wrapper;

  <span class="hljs-keyword">end</span> try
  <span class="hljs-keyword">begin</span> catch
    <span class="hljs-keyword">select</span>
      error_number() <span class="hljs-keyword">as</span> error_number,
      error_message() <span class="hljs-keyword">as</span> error_message,
      error_line() <span class="hljs-keyword">as</span> error_line,
      error_procedure() <span class="hljs-keyword">as</span> error_procedure
    <span class="hljs-keyword">for</span> <span class="hljs-keyword">json</span> <span class="hljs-keyword">path</span>, without_array_wrapper;
  <span class="hljs-keyword">end</span> catch
<span class="hljs-keyword">end</span>

<span class="hljs-keyword">go</span>
</code></pre>
<p><strong>Sample Usage</strong><br /><code>exec GetPatientSumInfo @dob = '1980-05-01', @zip = '12345'</code></p>
<p><strong>Sample Output</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"patientid"</span>: <span class="hljs-number">123</span>,
  <span class="hljs-attr">"first_name"</span>: <span class="hljs-string">"John"</span>,
  <span class="hljs-attr">"last_name"</span>: <span class="hljs-string">"Doe"</span>,
  <span class="hljs-attr">"dob"</span>: <span class="hljs-string">"1980-05-01"</span>,
  <span class="hljs-attr">"address"</span>: [
    {
      <span class="hljs-attr">"addr_id"</span>: <span class="hljs-number">789</span>,
      <span class="hljs-attr">"street"</span>: <span class="hljs-string">"123 Main St"</span>,
      <span class="hljs-attr">"city"</span>: <span class="hljs-string">"Springfield"</span>,
      <span class="hljs-attr">"state"</span>: <span class="hljs-string">"IL"</span>,
      <span class="hljs-attr">"zip"</span>: <span class="hljs-string">"12345"</span>
    }
  ]
}
</code></pre>
<p><strong>Error Handling</strong><br />If any unexpected error happens (e.g., bad join, null issue, query failure, etc.), the CATCH block will return a JSON response like this:</p>
<pre><code class="lang-sql">{
  "error_number": 208,
  "error_message": "Invalid object name 'patinfo'.",
  "error_line": 7,
  "error_procedure": "GetPatientSumInfo"
}
</code></pre>
<p>This is great for logging or for passing through to an API that displays a friendly error message to the caller.</p>
<p><strong>Performance Comparison</strong><br />I like using the CTEs + UNION approach because it keeps the code clean and easy to maintain, but it is not always the fastest option, especially in systems with a lot of traffic or big datasets.</p>
<p>Depending on the situation, I sometimes consider other approaches. Here is a quick comparison I use when deciding what fits best:</p>
<p>CTEs + UNION</p>
<ul>
<li><p>Performance: Medium</p>
</li>
<li><p>Maintainability: Excellent</p>
</li>
<li><p>Best Use: Clean modular logic with de-duplication</p>
</li>
<li><p>Notes: Extra cost for de-dupe</p>
</li>
</ul>
<p>CTEs + UNION ALL</p>
<ul>
<li><p>Performance: Good</p>
</li>
<li><p>Maintainability: Excellent</p>
</li>
<li><p>Best Use: Clean logic when overlap is impossible</p>
</li>
<li><p>Notes: Slightly faster if safe to use</p>
</li>
</ul>
<p>Single SELECT + OR conditions</p>
<ul>
<li><p>Performance: Fastest</p>
</li>
<li><p>Maintainability: Moderate</p>
</li>
<li><p>Best Use: High-performance environments</p>
</li>
<li><p>Notes: Harder to maintain and scale</p>
</li>
</ul>
<p>Temp table / Table variable</p>
<ul>
<li><p>Performance: Good</p>
</li>
<li><p>Maintainability: Moderate</p>
</li>
<li><p>Best Use: Multi-step logic or complex joins</p>
</li>
<li><p>Notes: Good for control, indexing flexibility</p>
</li>
</ul>
<p>Indexed view</p>
<ul>
<li><p>Performance: Very Fast</p>
</li>
<li><p>Maintainability: Hard to manage</p>
</li>
<li><p>Best Use: High-volume repeat queries</p>
</li>
<li><p>Notes: Overhead on inserts, complex setup</p>
</li>
</ul>
<p><strong>Final Thoughts</strong><br />This kind of flexible stored procedure has worked really well for me in situations where I need to match records using different types of information. By combining optional parameters, CTEs, and JSON output, I get something that is clean, modern, and easy to connect to an API.</p>
<p>If performance ever becomes a concern, there are some good ways to optimize without throwing out the whole design. I can switch from UNION to UNION ALL if I know the match paths are mutually exclusive. I can also cache commonly used reference data in the application layer, or preload smaller datasets into temp tables inside the procedure. And in heavier systems, I might use indexed views to speed up the matching process by querying a flattened, pre-joined structure.</p>
<p>But for most systems I have worked on, starting with this kind of clean, modular, CTE-based design provides a solid foundation. It is easy to read, easy to extend, and flexible enough to support all kinds of input combinations.</p>
]]></content:encoded></item><item><title><![CDATA[Dynamic API Dispatching in C#]]></title><description><![CDATA[When building an API, one of the first things you need to figure out is how to route incoming requests to the right piece of code. This is usually done by checking which fields are present in the request and deciding which method to run. For simple A...]]></description><link>https://blog.seandrew.info/dynamic-api-dispatching-in-c</link><guid isPermaLink="true">https://blog.seandrew.info/dynamic-api-dispatching-in-c</guid><category><![CDATA[C#]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Tue, 24 Jun 2025 16:11:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750781411391/ba23085e-eb08-4dc0-bfa3-f663a702f9ef.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building an API, one of the first things you need to figure out is how to route incoming requests to the right piece of code. This is usually done by checking which fields are present in the request and deciding which method to run. For simple APIs, this is often handled with a few if or switch statements. But once your inputs start to vary and grow more complex, this approach becomes hard to manage.</p>
<p>In my case, I needed to build an API endpoint that could handle multiple types of patient-related data. Sometimes a request had a PatientIdNum, other times it had just a date of birth and zip code, or maybe an invoice number. I wanted a way to let the API decide which method to call based on what was sent, without having to write a long list of conditions in code. So I went with a rule-based approach using a feature in C# called reflection.</p>
<p><strong>What is Dispatching?</strong><br />"Dispatching" simply means sending something to the right place. In APIs, this usually means figuring out which method should handle a request. A "dispatcher" is just a piece of code that makes this decision.</p>
<p>In my case, I wrote a dispatcher that checks the incoming JSON request, matches it to a rule in a JSON file, and then calls the matching method by name. This keeps the routing logic separate from the rest of the business logic and makes it easier to change.</p>
<p><strong>The Problem with Hardcoded Logic</strong><br />If you've ever written code like this, you know how quickly it can grow:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">if</span> (request.PatientIdNum != <span class="hljs-literal">null</span>)
  <span class="hljs-keyword">return</span> HandleByPatientIdNum(request);
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (request.PatInfo?.Dob != <span class="hljs-literal">null</span> &amp;&amp; request.PatInfo?.Zip != <span class="hljs-literal">null</span>)
  <span class="hljs-keyword">return</span> HandleByDemographics(request);
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (request.OmInfo?.FirstOrDefault()?.InvoiceNo != <span class="hljs-literal">null</span>)
  <span class="hljs-keyword">return</span> HandleByInvoice(request);
<span class="hljs-keyword">else</span>
  <span class="hljs-keyword">return</span> BadRequest(<span class="hljs-string">"Insufficient identifiers"</span>);
</code></pre>
<p>This kind of logic is fine at first, but over time it:<br />• Gets harder to read and maintain<br />• Tangles routing and business logic together<br />• Requires code changes every time the JSON input and rules change</p>
<p><strong>A Simpler Way: Rule-Based Routing</strong><br />With rule-based routing, you define your routing rules outside of your code, usually in a file. For me, that file is "rules.json". Each rule lists the fields required to trigger a handler method. Here's an example:</p>
<pre><code class="lang-json">[
  {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"HandleByPatientIdNum"</span>,
    <span class="hljs-attr">"match"</span>: [<span class="hljs-string">"PatientIdNum"</span>]
  },
  {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"HandleByDemographics"</span>,
    <span class="hljs-attr">"match"</span>: [<span class="hljs-string">"patinfo.dob"</span>, <span class="hljs-string">"patinfo.zip"</span>]
  },
  {
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"HandleByInvoice"</span>,
    <span class="hljs-attr">"match"</span>: [<span class="hljs-string">"ominvoice.invoice_no"</span>]
  }
]
</code></pre>
<p>Basically, each rule says: If these fields are present and not null, then run the method with this "name".</p>
<p><strong>How Reflection Helps</strong><br />Reflection is a .NET feature that lets your code look at itself while it's running. That means you can find a method by name and call it even if you didn't hardcode it directly. So, if the rule says to call "HandleByPatientIdNum", and that method exists in your controller, you can call it just by using its name (Reflection).</p>
<p>This makes the system very flexible. To add new functionality, you just write a new method and add a new rule to the JSON file (rules.json).</p>
<p><strong>Reflection Scope Note:</strong><br />For safety and clarity, I restrict the reflection lookup to non-public instance methods only using BindingFlags.NonPublic | BindingFlags.Instance. This avoids exposing public framework or utility methods unintentionally and keeps the dynamic invocation limited to the intended handler methods.</p>
<p><strong>How It Works</strong><br />The dispatcher loads the rules from the JSON file.</p>
<p>Rule Validation:<br />Before evaluating incoming requests, I run a simple preflight validator to check for common mistakes in the "rules.json" file. This helps catch issues early and avoid runtime surprises. The validator:<br />• Confirms all name entries refer to actual methods (using GetMethod(...))<br />• Ensures match arrays are not empty<br />• Optionally checks for duplicate or overly generic rules (e.g., rules that match any input)</p>
<p>This step is run at app startup and helps ensure all routing rules are both valid and intentional.</p>
<p>Then:</p>
<ol>
<li><p>The request comes in as nested JSON</p>
</li>
<li><p>The JSON is flattened into a dictionary with keys like patinfo.dob or ominvoice.invoice_no</p>
</li>
<li><p>The dispatcher loads the validated rules from rules.json</p>
</li>
<li><p>It checks each rule to see if all required fields are present and not null</p>
</li>
<li><p>It picks the most specific matching rule</p>
</li>
<li><p>It uses reflection to call the method named in the rule ("name")</p>
</li>
</ol>
<p><strong>Flattening the JSON input</strong><br />You cannot easily check for field presence like "patinfo.dob" or "ominvoice.invoice_no" with a simple dictionary lookup because those values are nested inside JSON objects.</p>
<p>Flattening turns nested structures into a flat dictionary where the keys represent the full JSON path using dot notation, which makes things much easier.</p>
<p>Here is an example of a nested JSON request:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"patinfo"</span>: {
    <span class="hljs-attr">"dob"</span>: <span class="hljs-string">"1980-05-01"</span>,
    <span class="hljs-attr">"address"</span>: {
      <span class="hljs-attr">"zip"</span>: <span class="hljs-string">"12345"</span>
    }
  },
  <span class="hljs-attr">"ominvoice"</span>: {
    <span class="hljs-attr">"invoice_no"</span>: <span class="hljs-string">"INV123"</span>
  }
}
</code></pre>
<p>And the dictionary (flattened) representation of the JSON input.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"patinfo.dob"</span>: <span class="hljs-string">"1980-05-01"</span>,
  <span class="hljs-attr">"patinfo.address.zip"</span>: <span class="hljs-string">"12345"</span>,
  <span class="hljs-attr">"ominvoice.invoice_no"</span>: <span class="hljs-string">"INV123"</span>
}
</code></pre>
<p>Each key is a string path pointing to the original value in the nested JSON which is easier to check instead of writing complex null checks or traversal code.</p>
<p>Example Dispatcher Code<br />This sample code uses the flattened dictionary to check against the rules in "rules.json" and then calls the right method.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Find the first rule that matches ALL of its required fields</span>
<span class="hljs-keyword">var</span> matchedRule = rules
  <span class="hljs-comment">// Keep only the rules where every required field in the rule's `Match` array:</span>
  <span class="hljs-comment">//   - Exists in the flattened input dictionary</span>
  <span class="hljs-comment">//   - Is not null</span>
  .Where(r =&gt; r.Match.All(field =&gt;
      flattenedInput.ContainsKey(field) &amp;&amp; flattenedInput[field] != <span class="hljs-literal">null</span>))

  <span class="hljs-comment">// If multiple rules match, prefer the one with the most required fields</span>
  <span class="hljs-comment">// (i.e., the most specific match)</span>
  .OrderByDescending(r =&gt; r.Match.Count)

  <span class="hljs-comment">// Return the first matching rule, or null if none match</span>
  .FirstOrDefault();

<span class="hljs-comment">// If no rule matched, return a 400 Bad Request</span>
<span class="hljs-keyword">if</span> (matchedRule == <span class="hljs-literal">null</span>)
  <span class="hljs-keyword">return</span> BadRequest(<span class="hljs-string">"No matching handler found"</span>);

<span class="hljs-comment">// Use reflection to locate a method on this controller with the same name</span>
<span class="hljs-comment">// as the rule's `name` field. Assume all handlers are private instance methods.</span>
<span class="hljs-keyword">var</span> method = <span class="hljs-keyword">this</span>.GetType().GetMethod(
    matchedRule.Name,
    BindingFlags.NonPublic | BindingFlags.Instance
);

<span class="hljs-comment">// If the method doesn't exist (e.g., typo in rules.json), return error</span>
<span class="hljs-keyword">if</span> (method == <span class="hljs-literal">null</span>)
  <span class="hljs-keyword">return</span> BadRequest(<span class="hljs-string">$"Handler method '<span class="hljs-subst">{matchedRule.Name}</span>' not found"</span>);

<span class="hljs-comment">// Dynamically invoke the matched handler method and pass the `request` object</span>
<span class="hljs-comment">// This assumes all handler methods follow the signature: </span>
<span class="hljs-comment">//   Task&lt;IActionResult&gt; MethodName(PatientRequest request)</span>
<span class="hljs-keyword">var</span> result = <span class="hljs-keyword">await</span> (Task&lt;IActionResult&gt;)method.Invoke(
    <span class="hljs-keyword">this</span>,
    <span class="hljs-keyword">new</span> <span class="hljs-keyword">object</span>[] { request }
);

<span class="hljs-comment">// Return the handler’s result</span>
<span class="hljs-keyword">return</span> result;
</code></pre>
<p>And the matching handler methods look like this:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">HandleByPatientIdNum</span>(<span class="hljs-params">PatientRequest request</span>)</span>
{
  <span class="hljs-comment">// code here</span>
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">HandleByDemographics</span>(<span class="hljs-params">PatientRequest request</span>)</span>
{
  <span class="hljs-comment">// code here</span>
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">HandleByInvoice</span>(<span class="hljs-params">PatientRequest request</span>)</span>
{
  <span class="hljs-comment">// code here</span>
}
</code></pre>
<p><strong>Why This Is Useful</strong><br />• You can change the rules without touching the code.<br />• Helps keep routing logic out of the controller methods.<br />• Avoids giant if-else/switch blocks.<br />• Easy to scale by just adding new methods and rules.</p>
<p><strong>Final Thoughts</strong><br />This rule-based dispatch system has made it much easier for me to handle flexible JSON inputs without burying myself in if-then/switch conditions. By flattening the input to a dictionary and matching fields to rules, I let my code decide what to do based on the data. It's clean, adaptable, and easy to extend. If your API needs to handle many different input patterns, this could be a great way to go.</p>
<p><strong>Notes</strong></p>
<p>While externalizing (rules.json) routing rules offers flexibility, the rules themselves must be managed carefully to avoid becoming a new source of complexity.</p>
<p>The performance benefit of caching method delegates is minimal due to low traffic and simplicity priorities. Reflection is used in a targeted, internal, and predictable way, and the current preference is to keep the dispatch logic simple, stateless, and easier to debug. No performance regressions have been observed in real use, and preliminary profiling showed that JSON parsing and validation dominate the request cycle. However, the architecture leaves room for caching if scale or latency concerns arise. Should performance profiling indicate a need, caching can be introduced in a controlled and validated manner later.</p>
]]></content:encoded></item><item><title><![CDATA[Identifying Stored Procedures Created or Modified Within a Date Range in SQL Server]]></title><description><![CDATA[As part of rewriting the documentation for my custom API, I needed to test each endpoint to ensure it behaved as expected, including validating both input and output. During this process, I discovered that several existing stored procedures required ...]]></description><link>https://blog.seandrew.info/identifying-stored-procedures-created-or-modified-within-a-date-range-in-sql-server</link><guid isPermaLink="true">https://blog.seandrew.info/identifying-stored-procedures-created-or-modified-within-a-date-range-in-sql-server</guid><category><![CDATA[SQL]]></category><category><![CDATA[SQL Server]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Wed, 18 Jun 2025 20:41:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750279202635/bd179dde-3712-486b-8319-4a289c5a60e7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As part of rewriting the documentation for my custom API, I needed to test each endpoint to ensure it behaved as expected, including validating both input and output. During this process, I discovered that several existing stored procedures required modification. Unfortunately, I did not keep track of which procedures were changed. Once the documentation was complete, I had to go back and identify the stored procedures that had been altered.</p>
<p>This experience led me to think more seriously about how to track changes to stored procedures; something that would be valuable for auditing, deployment reviews, and debugging. Having visibility into when a stored procedure was created or last modified can help both developers and database administrators identify recent stored procedure changes.</p>
<p>To address this, I took a look at SQL Server's system catalog views. I decided on the sys.procedures view which contain metadata such as creation and modification dates for stored procedures. This brief write-up outlines how to use T-SQL to retrieve a list of stored procedures that were either created or modified within a specified date range, including the script I wrote to accomplish this task.</p>
<p><strong>The TSQL Query</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">use</span> [yourdatabase]
<span class="hljs-keyword">go</span>

<span class="hljs-comment">-- select SPs that were created or modified within a specific date range</span>
<span class="hljs-keyword">select</span>
  schema_name(schema_id) <span class="hljs-keyword">as</span> schema_name, <span class="hljs-comment">-- retrieve schema name (e.g., 'dbo') for the procedure</span>
  <span class="hljs-keyword">name</span> <span class="hljs-keyword">as</span> procedure_name, <span class="hljs-comment">-- the name of the stored procedure</span>
  create_date, <span class="hljs-comment">-- the date the procedure was originally created</span>
  modify_date <span class="hljs-comment">-- the date the procedure was last altered</span>
<span class="hljs-keyword">from</span> sys.procedures <span class="hljs-comment">-- contains metadata for user-defined procedures</span>
<span class="hljs-keyword">where</span> create_date <span class="hljs-keyword">between</span> <span class="hljs-string">'2025-06-01'</span> <span class="hljs-keyword">and</span> <span class="hljs-string">'2025-06-18'</span> <span class="hljs-keyword">or</span> modify_date <span class="hljs-keyword">between</span> <span class="hljs-string">'2025-06-01'</span> <span class="hljs-keyword">and</span> <span class="hljs-string">'2025-06-18'</span>
<span class="hljs-keyword">order</span> <span class="hljs-keyword">by</span> modify_date <span class="hljs-keyword">desc</span>
</code></pre>
<p><strong>What This Query Does</strong><br />It retrieves a list of stored procedures from your selected SQL database that meet either of the following criteria:</p>
<p>• Created or altered between June 06 2025 and June 18 2025<br />• Each row in the result set includes:<br />• The schema name (such as dbo)<br />• The procedure name<br />• The creation date<br />• The last modification date</p>
<p>The results are sorted in descending order by modify_date (most recent appear at the top).</p>
<p><strong>Additional Notes</strong><br />Each database has their own "sys.procedures" and "sys.objects".</p>
<p>If you want to find other objects besides stored procedures you can use "sys.objects" instead of "sys.procedures". Example: Specify type = 'P' with sys.objects to limit the results to stored procedures.</p>
<p>Here is a query to find other object types such as triggers, user tables, etc.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span>
<span class="hljs-keyword">distinct</span>
<span class="hljs-keyword">type</span>,
type_desc
<span class="hljs-keyword">from</span> sys.objects
</code></pre>
<p>type type_desc<br />TT TYPE_TABLE<br />FN SQL_SCALAR_FUNCTION<br />UQ UNIQUE_CONSTRAINT<br />SQ SERVICE_QUEUE<br />F FOREIGN_KEY_CONSTRAINT<br />U USER_TABLE<br />D DEFAULT_CONSTRAINT<br />PK PRIMARY_KEY_CONSTRAINT<br />V VIEW<br />S SYSTEM_TABLE<br />IT INTERNAL_TABLE<br />P SQL_STORED_PROCEDURE<br />TR SQL_TRIGGER</p>
<p><strong>Conclusion</strong><br />SQL Server makes it easy to find out which stored procedures have been added or changed recently. Using the built-in system views, you can quickly get a list of procedures based on when they were created or last modified. This is helpful when trying to audit recent changes, review what went out in a deployment, or troubleshoot something that suddenly stopped working. Pretty good with keeping track of changes to help with things like documentation.</p>
]]></content:encoded></item><item><title><![CDATA[Customizing Output Caching in ASP.NET Web Forms and C# APIs]]></title><description><![CDATA[Efficient performance is essential for responsive, scalable web applications and one way to achieve that and boost performance is through caching. By avoiding repetitive data processing and rendering, caching allows applications to serve content more...]]></description><link>https://blog.seandrew.info/customizing-output-caching-in-aspnet-web-forms-and-c-apis</link><guid isPermaLink="true">https://blog.seandrew.info/customizing-output-caching-in-aspnet-web-forms-and-c-apis</guid><category><![CDATA[C#]]></category><category><![CDATA[c#]]></category><category><![CDATA[asp.net core]]></category><category><![CDATA[ASP.NET]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Fri, 30 May 2025 13:59:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748613453687/f0a0438f-3bf6-4087-bda8-9faaba63cca0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Efficient performance is essential for responsive, scalable web applications and one way to achieve that and boost performance is through caching. By avoiding repetitive data processing and rendering, caching allows applications to serve content more efficiently. In <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Forms and C# API-based applications, output caching and data caching can help improve load times and reduce server resource usage when applied appropriately.</p>
<p>Good Fit for Caching</p>
<ul>
<li><p>Data is read often (rarely written)</p>
</li>
<li><p>Content is shared across users</p>
</li>
<li><p>Calls are slow or expensive</p>
</li>
<li><p>Data freshness is not business-critical</p>
</li>
</ul>
<p>Not a Good Fit for Caching</p>
<ul>
<li><p>Data is updated frequently or in real-time</p>
</li>
<li><p>Data is personalized or user-specific</p>
</li>
<li><p>Data is already fast or trivial to retrieve</p>
</li>
<li><p>Data freshness is business-critical</p>
</li>
</ul>
<p><strong>Some Benefits of Using Caching</strong></p>
<ul>
<li><p>Reduce server load. By serving cached responses, the server uses fewer CPU, disk, and memory resources.</p>
</li>
<li><p>Improve User Experience. Serving cached responses will make response time faster, smoother, and more responsive.</p>
</li>
<li><p>Scales Better Under Load. Serving cached responses will help the system handle high volumes of traffic without performing full processing for each incoming request.</p>
</li>
</ul>
<p>Caching is a valuable performance tool, but it is not the only option. Other options such as asynchronous processing, batching, and queuing can also help improve responsiveness and reduce system load, especially when caching is not appropriate for situations where frequently updated data or tasks require the most current data.</p>
<p><strong>Why Use Caching to Boot Performance?</strong><br />Caching reduces the need to repeatedly perform expensive operations for things like database queries, page rendering, data serialization, and network and disk I/O.</p>
<ul>
<li><p>Querying a database, especially with complex joins or large datasets can be time-consuming. Caching the result set means subsequent requests can bypass the database entirely and deliver the same data as a cached response.</p>
</li>
<li><p>Calling an external API or service (weather, stock prices, third-party data over HTTP).</p>
</li>
<li><p>Database access over the network (querying a SQL database hosted on another machine or cloud service).</p>
</li>
<li><p>Accessing a microservice (sending requests between services in a distributed system).</p>
</li>
<li><p>Retrieving remote files (documents, images, PDFs, etc.)</p>
</li>
</ul>
<p>Computation or Business Logic<br />If a response depends on heavy processing (aggregating reports, business rules, etc.) caching can help avoid repeating that work for each request.</p>
<p>Rendering<br />Is typically for <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Form applications and is the process of generating HTML from server controls (or other content) that will be sent to the browser. In the context of <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Forms, this involves taking server-side controls (like GridView, Label, etc.) and converting them into HTML markup.</p>
<p>Serialization<br />Is typically for APIs and can be things like serializing return data into JSON or XML. Caching the final JSON or XML result (output caching) is pretty much skipping the rendering/serialization step for repeated (the same) inputs/outputs.</p>
<p>External Resource Access<br />Fetching data from third-party services or external APIs (weather, stock info, etc.) could introduce latency and maybe even rate limits. Caching these results reduces dependency on the third-party service and external API.</p>
<p>Network and Disk I/O<br />Reading static files or loading configuration from disk or across the network can also add delays. Caching them in memory will allow for quicker access to that information.</p>
<p><strong>Output Caching</strong><br />Output Caching in <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Forms<br />Output caching stores the final rendered HTML of a page and reuses it for subsequent requests. This is particularly effective for pages with static or content that rarely changes and can be represented as more static content.</p>
<p>To implement output caching in a Web Forms application, you can use the OutputCache directive:<br /><code>&lt;%@ OutputCache Duration="60" VaryByParam="none" %&gt;</code></p>
<ul>
<li><p>Duration defines how long (in seconds) the page is cached.</p>
</li>
<li><p>VaryByParam allows different versions of the cache based on query string or form parameters.</p>
</li>
</ul>
<p>For example, to cache different versions of a product page by productId for 120 seconds<br /><code>&lt;%@ OutputCache Duration="120" VaryByParam="productId" %&gt;</code></p>
<p>This ensures that each unique productId results in a distinct cached page.</p>
<p>You can also configure output caching programmatically in the page code-behind which provides more caching behavior control.</p>
<pre><code class="lang-csharp">    Response.Cache.SetExpires(DateTime.UtcNow.AddMinutes(<span class="hljs-number">2</span>));
    Response.Cache.SetCacheability(HttpCacheability.Public);
    Response.Cache.VaryByParams[<span class="hljs-string">"productId"</span>] = <span class="hljs-literal">true</span>;
</code></pre>
<p>Output Caching in C# APIs<br /><a target="_blank" href="http://ASP.NET">ASP.NET</a> Web API does not include built-in output caching like Web Forms does but basic HTTP response caching can be simulated by using a custom action filter. This filter sets caching headers on the response, allowing browsers or intermediary proxies to cache the result for a specified duration.</p>
<p>Here is a simple example defining a custom action filter:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">OutputCacheAttribute</span> : <span class="hljs-title">ActionFilterAttribute</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> <span class="hljs-keyword">int</span> _durationSeconds;

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">OutputCacheAttribute</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> durationSeconds</span>)</span>
  {
      _durationSeconds = durationSeconds;
  }

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">void</span> <span class="hljs-title">OnActionExecuted</span>(<span class="hljs-params">HttpActionExecutedContext context</span>)</span>
  {
    <span class="hljs-keyword">var</span> response = context.Response;
    <span class="hljs-keyword">if</span> (response != <span class="hljs-literal">null</span>)
    {
      response.Headers.CacheControl = <span class="hljs-keyword">new</span> CacheControlHeaderValue
      {
        Public = <span class="hljs-literal">true</span>,
        MaxAge = TimeSpan.FromSeconds(_durationSeconds)
      };
    }
  }
}
</code></pre>
<p>You would then use the custom action filter in a controller action doing something like this. This caches the response for 120 seconds and reduces the load on the backend for repeated requests with the same parameters.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">OutputCache(120)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> IHttpActionResult <span class="hljs-title">GetProduct</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> id</span>)</span>
{
  <span class="hljs-keyword">var</span> product = _productService.GetProductById(id);
  <span class="hljs-keyword">return</span> Ok(product);
}
</code></pre>
<p><strong>Data Caching</strong><br />In addition to output caching, data caching offers greater flexibility and can be used effectively in both Web Forms and APIs. Data caching is the storing of data temporarily in memory so that it can be retrieved faster the next time it is needed. This reduces expensive operations such as repeated database calls.</p>
<p>Data Caching for <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Forms<br />In an <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Form application, data caching is implemented using the "System.Web.Caching.Cache" class. Example of getting a list of products and cache it for 5 minutes and then reuse it to avoid repeated "database" access.</p>
<p>ProductRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ProductRepository</span>
{
  <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">GetProducts</span>(<span class="hljs-params"></span>)</span>
  {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">string</span>&gt; { <span class="hljs-string">"Apple"</span>, <span class="hljs-string">"Banana"</span>, <span class="hljs-string">"Cherry"</span> };
  }
}
</code></pre>
<p>Default.aspx.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">partial</span> <span class="hljs-keyword">class</span> _<span class="hljs-title">Default</span> : <span class="hljs-title">System.Web.UI.Page</span>
{
  <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Page_Load</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, EventArgs e</span>)</span>
  {
    <span class="hljs-keyword">if</span> (!IsPostBack)
    {
      <span class="hljs-keyword">var</span> products = GetProductsFromCache();
      ListBox1.DataSource = products;
      ListBox1.DataBind();
    }
  }

  <span class="hljs-function"><span class="hljs-keyword">private</span> List&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">GetProductsFromCache</span>(<span class="hljs-params"></span>)</span>
  {
    <span class="hljs-keyword">var</span> products = Cache[<span class="hljs-string">"ProductList"</span>] <span class="hljs-keyword">as</span> List&lt;<span class="hljs-keyword">string</span>&gt;;
    <span class="hljs-keyword">if</span> (products == <span class="hljs-literal">null</span>)
    {
      <span class="hljs-keyword">var</span> repo = <span class="hljs-keyword">new</span> ProductRepository();
      products = repo.GetProducts();
      Cache.Insert(<span class="hljs-string">"ProductList"</span>, products, <span class="hljs-literal">null</span>, DateTime.Now.AddMinutes(<span class="hljs-number">5</span>), System.Web.Caching.Cache.NoSlidingExpiration);
    }
    <span class="hljs-keyword">return</span> products;
  }
}
</code></pre>
<p>Default.aspx<br /><code>&lt;asp:ListBox ID="ListBox1" runat="server" Width="200"&gt;&lt;/asp:ListBox&gt;</code></p>
<p>Data Caching in an <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core API<br />In <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core APIs, data caching is commonly implemented using the built-in IMemoryCache service. This allows you to store frequently requested data (such as database query results) in memory to reduce redundant database calls and improve performance. Example of getting a list of products and cache it for 5 minutes and then reuse it to avoid repeated "database" access.</p>
<p>ProductRepository.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ProductRepository</span>
{
  <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">GetProducts</span>(<span class="hljs-params"></span>)</span>
  {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">string</span>&gt; { <span class="hljs-string">"Apple"</span>, <span class="hljs-string">"Banana"</span>, <span class="hljs-string">"Cherry"</span> };
  }
}
</code></pre>
<p>ProductController.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> Microsoft.AspNetCore.Mvc;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Caching.Memory;

[<span class="hljs-meta">ApiController</span>]
[<span class="hljs-meta">Route(<span class="hljs-meta-string">"products"</span>)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ProductsController</span> : <span class="hljs-title">ControllerBase</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IMemoryCache _cache;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ProductRepository _repo = <span class="hljs-keyword">new</span> ProductRepository();

  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">ProductsController</span>(<span class="hljs-params">IMemoryCache cache</span>)</span>
  {
    _cache = cache;
  }

  [<span class="hljs-meta">HttpGet</span>]
  <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">Get</span>(<span class="hljs-params"></span>)</span>
  {
    <span class="hljs-keyword">if</span> (!_cache.TryGetValue(<span class="hljs-string">"ProductList"</span>, <span class="hljs-keyword">out</span> List&lt;<span class="hljs-keyword">string</span>&gt; products))
    {
      products = _repo.GetProducts();
      _cache.Set(<span class="hljs-string">"ProductList"</span>, products, TimeSpan.FromMinutes(<span class="hljs-number">5</span>));
    }

    <span class="hljs-keyword">return</span> products;
  }
}
</code></pre>
<p>Program.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddControllers();

<span class="hljs-keyword">var</span> app = builder.Build();
app.MapControllers();
app.Run();
</code></pre>
<p><strong>Conclusion</strong><br />Caching is a good way to boost <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Forms and C# API application performance.</p>
<p>Output caching is a good way to speed up an entire page or response and is ideal for quickly serving complete pages or responses.<br />Data Caching is a good way to improve business logic or data access performance and supports reusability and responsiveness at the business logic layer.</p>
<p>Also, it is handy that you can control how long things stay in the cache and even change what gets cached based on things like query strings or user roles. Basically, you can cache what makes sense when it makes sense and also refresh the cache when required.</p>
]]></content:encoded></item><item><title><![CDATA[Filtering and Exporting SQL Stored Procedures with PowerShell]]></title><description><![CDATA[When managing SQL Server stored procedures, there are times when I need to quickly generate scripts for them, usually for backups, migrations, a specific client or functionality, or documentation. In this document, I show how I use T-SQL and PowerShe...]]></description><link>https://blog.seandrew.info/filtering-and-exporting-sql-stored-procedures-with-powershell</link><guid isPermaLink="true">https://blog.seandrew.info/filtering-and-exporting-sql-stored-procedures-with-powershell</guid><category><![CDATA[SQL]]></category><category><![CDATA[SQL Server]]></category><category><![CDATA[Powershell]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Tue, 25 Mar 2025 13:52:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742910656712/b2a79de1-1983-4677-839e-78fcf4a54708.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When managing SQL Server stored procedures, there are times when I need to quickly generate scripts for them, usually for backups, migrations, a specific client or functionality, or documentation. In this document, I show how I use T-SQL and PowerShell to filter stored procedures based on naming patterns and export them into individual .sql files. Each file contains the full DROP IF EXISTS and CREATE script which allows for the easy recreation of the stored procedures.</p>
<p>Running the script. The user account executing this script will require permissions to connect to the target database and read from the "sys.procedures" and "sys.sql_modules" system tables. Additionally, the account must have write permissions for the designated output file directory.</p>
<p><strong>The Script</strong></p>
<pre><code class="lang-powershell"><span class="hljs-comment"># Define the SQL Server connection string</span>
<span class="hljs-variable">$connectionString</span> = <span class="hljs-string">"Server=SQLServerName;Database=DBName;Integrated Security=True"</span>

 <span class="hljs-comment"># Define the output folder for stored procedure scripts</span>
<span class="hljs-variable">$outputFolder</span> = <span class="hljs-string">"C:\API_SPs"</span> <span class="hljs-comment"># Can also be a network UNC</span>
<span class="hljs-variable">$logFile</span> = <span class="hljs-string">"<span class="hljs-variable">$outputFolder</span>\ErrorLog.txt"</span> <span class="hljs-comment"># Log file to store errors</span>
<span class="hljs-variable">$errorsOccurred</span> = <span class="hljs-variable">$false</span>  <span class="hljs-comment"># Flag to track if any errors occurred</span>
<span class="hljs-variable">$scriptsWritten</span> = <span class="hljs-variable">$false</span>  <span class="hljs-comment"># Flag to track if any scripts were actually written</span>

<span class="hljs-comment"># Ensure output folder exists, create it if necessary</span>
<span class="hljs-keyword">if</span> (!(<span class="hljs-built_in">Test-Path</span> <span class="hljs-variable">$outputFolder</span>))
{
  <span class="hljs-built_in">New-Item</span> <span class="hljs-literal">-ItemType</span> Directory <span class="hljs-literal">-Path</span> <span class="hljs-variable">$outputFolder</span> <span class="hljs-literal">-Force</span> | <span class="hljs-built_in">Out-Null</span>
}

<span class="hljs-comment"># Define SQL query to retrieve stored procedure definitions</span>
<span class="hljs-variable">$query</span> = <span class="hljs-string">@"
SELECT 
 p.name AS ProcedureName,
 s.name AS SchemaName,
 'DROP PROCEDURE IF EXISTS [' + s.name + '].[' + p.name + '];' + CHAR(13) + CHAR(10) +
 'GO' + CHAR(13) + CHAR(10) +
 sm.definition + CHAR(13) + CHAR(10) +
 'GO' AS ScriptContent
FROM sys.procedures p
JOIN sys.schemas s ON p.schema_id = s.schema_id
JOIN sys.sql_modules sm ON p.object_id = sm.object_id
WHERE 
(
 p.name LIKE 'XXX_YYY%' 
 AND p.name NOT IN ('%BACKUP%', '%OLD%')
)
"@</span>

<span class="hljs-comment"># Function to log errors to a file and display in the console</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Log-Error</span></span>
{
  <span class="hljs-keyword">param</span> ([<span class="hljs-built_in">string</span>]<span class="hljs-variable">$message</span>)
  <span class="hljs-variable">$timestamp</span> = <span class="hljs-built_in">Get-Date</span> <span class="hljs-literal">-Format</span> <span class="hljs-string">"yyyy-MM-dd HH:mm:ss"</span>
  <span class="hljs-variable">$logEntry</span> = <span class="hljs-string">"<span class="hljs-variable">$timestamp</span> - ERROR: <span class="hljs-variable">$message</span>"</span>
  <span class="hljs-built_in">Add-Content</span> <span class="hljs-literal">-Path</span> <span class="hljs-variable">$logFile</span> <span class="hljs-literal">-Value</span> <span class="hljs-variable">$logEntry</span>  <span class="hljs-comment"># Append error to log file</span>

  <span class="hljs-comment"># Print error directly to console</span>
  <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"ERROR: <span class="hljs-variable">$message</span>"</span> <span class="hljs-literal">-ForegroundColor</span> Red  

  <span class="hljs-comment"># Mark that an error has occurred</span>
  <span class="hljs-variable">$global:errorsOccurred</span> = <span class="hljs-variable">$true</span>
}

<span class="hljs-comment"># Initialize SQL connection and execute query</span>
<span class="hljs-variable">$sqlConnection</span> = <span class="hljs-variable">$null</span>  <span class="hljs-comment"># Variable to hold the connection object</span>
<span class="hljs-variable">$procedures</span> = <span class="hljs-selector-tag">@</span>{}  <span class="hljs-comment"># Dictionary to store procedure names and their scripts</span>

<span class="hljs-keyword">try</span>
{
  <span class="hljs-comment"># Create and open SQL connection</span>
  <span class="hljs-variable">$sqlConnection</span> = <span class="hljs-built_in">New-Object</span> System.Data.SqlClient.SqlConnection(<span class="hljs-variable">$connectionString</span>)
  <span class="hljs-variable">$sqlConnection</span>.Open()

  <span class="hljs-comment"># Check if connection is successful</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-variable">$sqlConnection</span>.State <span class="hljs-operator">-ne</span> <span class="hljs-string">'Open'</span>)
  {
    <span class="hljs-keyword">throw</span> <span class="hljs-string">"Failed to open SQL connection."</span>
  }

  <span class="hljs-comment"># Execute the query</span>
  <span class="hljs-variable">$sqlCommand</span> = <span class="hljs-variable">$sqlConnection</span>.CreateCommand()
  <span class="hljs-variable">$sqlCommand</span>.CommandText = <span class="hljs-variable">$query</span>
  <span class="hljs-variable">$reader</span> = <span class="hljs-variable">$sqlCommand</span>.ExecuteReader()

  <span class="hljs-comment"># Read query results</span>
  <span class="hljs-keyword">while</span> (<span class="hljs-variable">$reader</span>.Read())
  {
    <span class="hljs-comment"># Ensure valid file names by replacing invalid characters</span>
    <span class="hljs-variable">$procedureName</span> = <span class="hljs-variable">$reader</span>[<span class="hljs-string">"ProcedureName"</span>] <span class="hljs-operator">-replace</span> <span class="hljs-string">'[\\\/:*?"&lt;&gt;|]'</span>, <span class="hljs-string">'_'</span> <span class="hljs-comment"># replace \\\/:*?"&lt;&gt;| with underscore</span>
    <span class="hljs-variable">$scriptContent</span> = <span class="hljs-variable">$reader</span>[<span class="hljs-string">"ScriptContent"</span>]
    <span class="hljs-variable">$procedures</span>[<span class="hljs-variable">$procedureName</span>] = <span class="hljs-variable">$scriptContent</span>  <span class="hljs-comment"># Store in PS hashtable/dictionary</span>
  }

  <span class="hljs-variable">$reader</span>.Close()  <span class="hljs-comment"># Close the SQL data reader</span>
}
<span class="hljs-keyword">catch</span>
{
  Log<span class="hljs-literal">-Error</span> <span class="hljs-string">"Database error: <span class="hljs-variable">$_</span>"</span>
}
<span class="hljs-keyword">finally</span>
{
  <span class="hljs-comment"># Ensure SQL connection is closed to avoid resource leaks</span>
  <span class="hljs-keyword">if</span> (<span class="hljs-variable">$sqlConnection</span> <span class="hljs-operator">-ne</span> <span class="hljs-variable">$null</span> <span class="hljs-operator">-and</span> <span class="hljs-variable">$sqlConnection</span>.State <span class="hljs-operator">-eq</span> <span class="hljs-string">'Open'</span>)
  {
    <span class="hljs-variable">$sqlConnection</span>.Close()
  }
}

<span class="hljs-comment"># Write stored procedure scripts to files only if there are procedures to save</span>
<span class="hljs-keyword">if</span> (<span class="hljs-variable">$procedures</span>.Count <span class="hljs-operator">-gt</span> <span class="hljs-number">0</span>)
{
  <span class="hljs-keyword">foreach</span> (<span class="hljs-variable">$procedureName</span> <span class="hljs-keyword">in</span> <span class="hljs-variable">$procedures</span>.Keys) <span class="hljs-comment"># $procedures is a PS hashtable/dictionary and .Keys retrieves all keys from the hashtable</span>
  {
    <span class="hljs-keyword">try</span>
    {
      <span class="hljs-variable">$filePath</span> = <span class="hljs-string">"<span class="hljs-variable">$outputFolder</span>\<span class="hljs-variable">$procedureName</span>.sql"</span>
      <span class="hljs-comment"># Write the script content to the file using UTF-8 encoding</span>
      [<span class="hljs-type">System.IO.File</span>]::WriteAllText(<span class="hljs-variable">$filePath</span>, <span class="hljs-variable">$procedures</span>[<span class="hljs-variable">$procedureName</span>], [<span class="hljs-type">System.Text.Encoding</span>]::UTF8)
      <span class="hljs-variable">$scriptsWritten</span> = <span class="hljs-variable">$true</span>  <span class="hljs-comment"># Mark that at least one script was successfully written</span>
    }
    <span class="hljs-keyword">catch</span>
    {
      Log<span class="hljs-literal">-Error</span> <span class="hljs-string">"Failed to write file: <span class="hljs-variable">$filePath</span> - <span class="hljs-variable">$_</span>"</span>
    }
  }
}

<span class="hljs-comment"># Final status message</span>
<span class="hljs-keyword">if</span> (<span class="hljs-variable">$errorsOccurred</span> <span class="hljs-operator">-and</span> <span class="hljs-operator">-not</span> <span class="hljs-variable">$scriptsWritten</span>)
{
    <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"No stored procedure scripts were saved. Errors occurred. See <span class="hljs-variable">$logFile</span> for details."</span> <span class="hljs-literal">-ForegroundColor</span> Yellow
}
<span class="hljs-keyword">elseif</span> (<span class="hljs-variable">$errorsOccurred</span> <span class="hljs-operator">-and</span> <span class="hljs-variable">$scriptsWritten</span>)
{
    <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"Stored procedure scripts have been saved to: <span class="hljs-variable">$outputFolder</span>, but some errors occurred. See <span class="hljs-variable">$logFile</span> for details."</span> <span class="hljs-literal">-ForegroundColor</span> Yellow
}
<span class="hljs-keyword">elseif</span> (<span class="hljs-variable">$scriptsWritten</span>)
{
    <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"Stored procedure scripts have been successfully saved to: <span class="hljs-variable">$outputFolder</span>"</span> <span class="hljs-literal">-ForegroundColor</span> Green
}
<span class="hljs-keyword">else</span>
{
    <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"No stored procedures matched the criteria. No scripts were generated."</span> <span class="hljs-literal">-ForegroundColor</span> Cyan
}
</code></pre>
<p><strong>Steps Performed by the Script</strong></p>
<ol>
<li><p>Connect to SQL Server: The script establishes a connection to the SQL Server using the provided credentials.</p>
</li>
<li><p>Query the Database: It executes a SQL query to retrieve a list of stored procedures that match the specified criteria (e.g., names starting with "CP_API").</p>
</li>
<li><p>Generate Drop and Create Scripts: For each stored procedure, the script constructs a SQL script containing the DROP PROCEDURE IF EXISTS and CREATE PROCEDURE statements, including the procedure's full definition.</p>
</li>
<li><p>Save to Files: The script generates an individual .sql file for each stored procedure and saves it to the designated directory (e.g., C:\SQLScripts).</p>
</li>
<li><p>Handle Exceptions: Any issues encountered during the execution (such as permission issues, missing procedures, cannot connect to SQL server) are handled by the script. Error messages are written to ErrorLog.txt.</p>
</li>
</ol>
<p><strong>Conclusion</strong><br />This PowerShell script provides an automated and efficient way to mass generate DROP and CREATE scripts for stored procedures in SQL Server, saving them as individual files for easy deployment or backup. By customizing the SQL query criteria, you can target specific stored procedures, such as those beginning with a particular prefix or excluding certain procedures from the output.</p>
<p>Using this script, database administrators or developers can quickly export the definitions of stored procedures for version control, migrations, or disaster recovery planning. This solution simplifies the process of managing stored procedure scripts, making it a valuable tool for SQL Server environments.</p>
]]></content:encoded></item><item><title><![CDATA[Some Random SQL Error Handling Techniques for Reliability and Transactional Integrity]]></title><description><![CDATA[Error handling is an important part of writing SQL code. Whether working with stored procedures, triggers, or transactions, proper error handling ensures that database operations remain reliable, maintainable, and capable of handling unexpected situa...]]></description><link>https://blog.seandrew.info/some-random-sql-error-handling-techniques-for-reliability-and-transactional-integrity</link><guid isPermaLink="true">https://blog.seandrew.info/some-random-sql-error-handling-techniques-for-reliability-and-transactional-integrity</guid><category><![CDATA[SQL]]></category><category><![CDATA[SQL Server]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Mon, 24 Feb 2025 20:24:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1740428646911/03058784-a006-4a5c-82da-39af2043b5c2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Error handling is an important part of writing SQL code. Whether working with stored procedures, triggers, or transactions, proper error handling ensures that database operations remain reliable, maintainable, and capable of handling unexpected situations. In this write-up, I put together some very basic samples of SQL error handling, including TRY...CATCH blocks, logging, transaction management, as well as SAVEPOINT, XACT_ABORT, and stored procedure return codes.</p>
<p>For simplicity, the T-SQL code snippets in this write-up are provided for illustrative purposes only and do not necessarily reflect best coding practices.</p>
<p><strong>1. Fundamentals of SQL Error Handling</strong><br /><strong>TRY...CATCH Block</strong><br />The TRY...CATCH block is the primary mechanism for handling errors in SQL Server. It allows capturing exceptions and responding appropriately. If an error occurs inside the TRY block, the CATCH block executes, returning error details in a select statement. If you need execution to stop, then you will need to throw the error instead on simply doing a select statement.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span> try
  <span class="hljs-comment">-- your main sql code here</span>
  <span class="hljs-comment">-- your main sql code here</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
  <span class="hljs-keyword">select</span>
  error_message() <span class="hljs-keyword">as</span> ErrorMessage,
  error_number() <span class="hljs-keyword">as</span> ErrorNumber,
  error_severity() <span class="hljs-keyword">as</span> ErrorSeverity,
  error_state() <span class="hljs-keyword">as</span> ErrorState
<span class="hljs-keyword">end</span> catch
</code></pre>
<p><strong>THROW and RAISEERROR</strong><br />RAISEERROR allows you to generate custom error messages with specified severity levels.</p>
<p>THROW is used to re-throw captured errors and halt execution.</p>
<p>The point of this code is to demonstrate conditional logic before raising an error in SQL Server using RAISERROR with TRY...CATCH error handling.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- example of conditional logic before raising an error</span>
<span class="hljs-keyword">declare</span> @somecondition <span class="hljs-built_in">int</span> = <span class="hljs-number">1</span> <span class="hljs-comment">-- this will cause the error to be thrown</span>

<span class="hljs-keyword">begin</span> try
  <span class="hljs-comment">-- conditionally raise an error</span>
  <span class="hljs-keyword">if</span> @somecondition = <span class="hljs-number">1</span> <span class="hljs-comment">-- if @somecondition &lt;&gt; 1 then error would not be thrown</span>
  <span class="hljs-keyword">begin</span>
    raiserror(<span class="hljs-string">'my custom error occurred due to some condition.'</span>, <span class="hljs-number">16</span>, <span class="hljs-number">1</span>) <span class="hljs-comment">-- raise error</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment">-- your main sql code here</span>
  <span class="hljs-keyword">select</span> <span class="hljs-string">'hello there'</span>
  <span class="hljs-keyword">select</span> <span class="hljs-string">'no error occurred'</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
  <span class="hljs-comment">-- capture the error and re-throw it</span>
  throw  <span class="hljs-comment">-- re-throw the error</span>
<span class="hljs-keyword">end</span> catch
</code></pre>
<p>Breaking Down the Concept of "Rethrow"</p>
<ol>
<li><p>An error occurs in the TRY block.</p>
</li>
<li><p>If @somecondition = 1, the THROW statement inside TRY raises an error.</p>
</li>
<li><p>Execution immediately jumps to the CATCH block.</p>
</li>
<li><p>The CATCH block captures the error.</p>
</li>
<li><p>SQL Server automatically provides access to error details (ERROR_MESSAGE(), ERROR_NUMBER(), etc.).</p>
</li>
<li><p>Just handling the error in CATCH does not automatically stop execution as it needs to be re-raised if necessary.</p>
</li>
<li><p>Using THROW in CATCH (Re-throwing the error)</p>
</li>
<li><p>Instead of logging the error or handling it silently, THROW raises the exact same error again.</p>
</li>
<li><p>This allows the error to propagate up to the caller (e.g., another stored procedure or application).</p>
</li>
<li><p>No need to pass error details manually because SQL Server remembers the error context.</p>
</li>
</ol>
<p>Why "Re-throw" Instead of Just "Throw"?</p>
<ul>
<li><p>The term "throw" generally refers to raising a new error.</p>
</li>
<li><p>The term "rethrow" is used when you catch an existing error and raise it again.</p>
</li>
</ul>
<p>When to Use Rethrow?</p>
<ul>
<li><p>When you don’t want to change the original error.</p>
</li>
<li><p>When you want the error to propagate up the call stack as if the TRY...CATCH was not there.</p>
</li>
<li><p>When handling errors in nested stored procedures or transaction rollback scenarios.</p>
</li>
</ul>
<p><strong>An alternative version of the SQL code using just THROW without RAISERROR</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">declare</span> @somecondition <span class="hljs-built_in">int</span> = <span class="hljs-number">1</span> <span class="hljs-comment">-- this will cause the error to be thrown</span>

<span class="hljs-keyword">begin</span> try
  <span class="hljs-comment">-- conditionally throw an error</span>
  <span class="hljs-keyword">if</span> @somecondition = <span class="hljs-number">1</span> <span class="hljs-comment">-- if @somecondition &lt;&gt; 1 then the error would not be thrown</span>
  <span class="hljs-keyword">begin</span>
    throw <span class="hljs-number">50001</span>, <span class="hljs-string">'my custom error occurred due to some condition.'</span>, <span class="hljs-number">1</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment">-- your main sql code here</span>
  <span class="hljs-keyword">select</span> <span class="hljs-string">'hello there'</span>
  <span class="hljs-keyword">select</span> <span class="hljs-string">'no error occurred'</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
  <span class="hljs-comment">-- capture the error and re-throw it</span>
  throw  <span class="hljs-comment">-- re-throw the error</span>
<span class="hljs-keyword">end</span> catch
</code></pre>
<p>Key Differences Between THROW and RAISERROR</p>
<ol>
<li><p>Simpler Syntax:</p>
</li>
<li><p>THROW 50001, 'My custom error occurred due to some condition.', 1</p>
</li>
<li><p>No need to specify severity (16) like in RAISERROR.</p>
</li>
<li><p>Built-in THROW for Re-Raising Errors:</p>
</li>
<li><p>THROW inside CATCH automatically re-throws the caught error without needing parameters.</p>
</li>
<li><p>RAISERROR requires capturing and re-raising the original error manually.</p>
</li>
<li><p>Always Uses Severity 16+:</p>
</li>
<li><p>With THROW, the severity must be 50000 or higher, which is the custom error range for SQL server.</p>
</li>
<li><p>RAISERROR allows you to specify lower severity levels.</p>
</li>
</ol>
<p>When to Use THROW Instead of RAISERROR?</p>
<ul>
<li><p>When you need simpler and cleaner error handling.</p>
</li>
<li><p>When you don't need advanced formatting. RAISERROR supports message substitution using placeholders.</p>
</li>
<li><p>When using TRY...CATCH blocks, THROW is the preferred way to re-raise exceptions.</p>
</li>
</ul>
<p><strong>2. Transaction Management and Atomicity</strong><br /><strong>Using TRY...CATCH with Transactions</strong><br />Transactions in SQL Server guarantee atomicity, meaning that all operations within a transaction either succeed as a group or fail as a group. If any part of the process fails, the system rolls back to its original state, preventing partial updates and ensuring database integrity.</p>
<p>Using transactions is crucial when executing multiple SQL statements that must either fully complete or not execute at all to maintain consistency.</p>
<p>Basic Transaction with Error Handling<br />This sample code shows how a transaction ensures atomicity. If a specific condition is met, an error is thrown which causes the transaction to roll back:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">declare</span> @somecondition <span class="hljs-built_in">int</span> = <span class="hljs-number">1</span> <span class="hljs-comment">-- this will cause the error to be thrown</span>

<span class="hljs-keyword">begin</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">begin</span> try
  <span class="hljs-keyword">if</span> @somecondition = <span class="hljs-number">1</span> <span class="hljs-comment">-- if @somecondition &lt;&gt; 1 then the error would not be thrown</span>
  <span class="hljs-keyword">begin</span>
    throw <span class="hljs-number">50001</span>, <span class="hljs-string">'my custom error occurred due to some condition.'</span>, <span class="hljs-number">1</span>
  <span class="hljs-keyword">end</span>

  <span class="hljs-comment">-- sql operations (insert, update, delete, etc.)</span>
  <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
  <span class="hljs-keyword">rollback</span> <span class="hljs-keyword">transaction</span>
  throw  <span class="hljs-comment">-- re-throw the error</span>
<span class="hljs-keyword">end</span> catch
</code></pre>
<p>If any SQL statement inside the TRY block fails, SQL Server immediately jumps to the CATCH block. The CATCH block rolls back the transaction, undoing any changes made since BEGIN TRANSACTION, thereby preventing partial updates.</p>
<p>Logging Errors in Transactions<br />Adding error logging to the CATCH block can help track failures and diagnose issues effectively.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">begin</span> try
  <span class="hljs-comment">-- some sql operations (insert, update, delete, etc.)</span>
  <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
    <span class="hljs-keyword">rollback</span> <span class="hljs-keyword">transaction</span>

    <span class="hljs-comment">-- log error details to a custom audit table</span>
    <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> errorlog (errormessage, errorprocedure, errorline, errortime)
    <span class="hljs-keyword">values</span> (ERROR_MESSAGE(), ERROR_PROCEDURE(), ERROR_LINE(), <span class="hljs-keyword">GETDATE</span>())

    <span class="hljs-keyword">select</span> ERROR_MESSAGE() <span class="hljs-keyword">as</span> errormessage
<span class="hljs-keyword">end</span> catch
</code></pre>
<p>Transactions are essential for atomicity and when performing multiple dependent SQL operations. ROLLBACK ensures data consistency by reverting changes in case of failure and the TRY…CATCH block allows for logging errors and prevents partial updates. This is important in all operations that interact with SQL and where data integrity is crucial.</p>
<p>Transactions ensure atomicity, which means that when performing multiple dependent SQL operations, they all must succeed together or fail together. ROLLBACK maintains consistency by reverting changes in case of failure and the TRY…CATCH block handles errors effectively while allowing for error logging and preventing partial updates. Logging errors provides valuable debugging information, ensuring better system reliability.</p>
<p>Transactions are essential in any SQL operation where data integrity is critical, ensuring that changes to the database remain consistent and reliable even in the event of failures.</p>
<p><strong>Using SAVEPOINT for Partial Transaction Rollbacks</strong><br />SAVEPOINT allows rolling back specific parts of a transaction while keeping others intact. In SQL Server, SAVEPOINT provides finer control over transactions by allowing partial rollbacks. Unlike a full ROLLBACK TRANSACTION, which undoes all changes since BEGIN TRANSACTION, SAVEPOINT enables rolling back only a portion of the transaction while keeping the rest intact. This is useful when handling multiple operations where only certain parts should be undone in case of an error.</p>
<p>SAVEPOINT can help preserve successful operations so if an error occurs, you can roll back to the last SAVEPOINT rather than discarding the entire transaction. This can allow for finer control when handling failures within a large transaction which can be helpful when dealing with dependent operations where certain steps should remain committed even if a later step fails and is rolled back.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">begin</span> try
  <span class="hljs-comment">-- insert new record into the orders table</span>
  <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> orders (orderid, customername, orderdate)
  <span class="hljs-keyword">values</span> (<span class="hljs-number">101</span>, <span class="hljs-string">'John Doe'</span>, <span class="hljs-keyword">getdate</span>())

  <span class="hljs-comment">-- define savepoint before inserting a new</span>
  <span class="hljs-comment">-- record into the orderdetails table</span>
  <span class="hljs-keyword">save</span> <span class="hljs-keyword">transaction</span> OrderDetailsSavepoint

  <span class="hljs-comment">-- insert new record into the orderdetails table</span>
  <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> orderdetails (orderid, productid, quantity)
  <span class="hljs-keyword">values</span> (<span class="hljs-number">101</span>, <span class="hljs-number">1</span>, <span class="hljs-number">5</span>)

  <span class="hljs-comment">-- simulate an error</span>
  throw <span class="hljs-number">50001</span>, <span class="hljs-string">'simulated error occurred after OrderDetails insertion.'</span>, <span class="hljs-number">1</span>

  <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
  <span class="hljs-keyword">select</span> <span class="hljs-string">'error. rolling back to OrderDetailsSavepoint savepoint.'</span>

  <span class="hljs-comment">-- rollback only the changes made after the savepoint</span>
  <span class="hljs-keyword">rollback</span> <span class="hljs-keyword">transaction</span> OrderDetailsSavepoint

  <span class="hljs-keyword">select</span> <span class="hljs-string">'continuing with remaining transaction...'</span>

  <span class="hljs-comment">-- you can still commit other successful operations</span>
  <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">end</span> catch
</code></pre>
<p>If a transaction is fully rolled back (full ROLLBACK TRANSACTION), all SAVEPOINTS within it are also discarded. Using SAVEPOINT does not release locks until the full transaction is committed or fully rolled back.</p>
<p><strong>Using XACT_ABORT for Automatic Rollbacks</strong><br />XACT_ABORT is a session-level (entire transaction) setting in SQL Server that automatically rolls back the entire transaction if a runtime error occurs. This eliminates the need for explicit error handling with ROLLBACK TRANSACTION which can help ensure data integrity with minimal code. It is very useful for bulk inserts, bulk updates and other "batch" type operations as it prevents partial updates in large batch operations.</p>
<p>XACT_ABORT ensures full rollback on failure. If any statement inside a transaction fails, the entire transaction is automatically rolled back. You do not need explicit BEGIN CATCH and ROLLBACK TRANSACTION with every transaction (reduces manual error handling).</p>
<pre><code class="lang-sql"><span class="hljs-keyword">set</span> xact_abort <span class="hljs-keyword">on</span> <span class="hljs-comment">-- turn it on</span>
<span class="hljs-comment">-- do not need to turn off xact_abort</span>
<span class="hljs-comment">-- because it applies to the current session or batch</span>
<span class="hljs-comment">-- after the session ends or a new connection is made</span>
<span class="hljs-comment">-- xact_abort resets to its default state (off).</span>

<span class="hljs-keyword">begin</span> <span class="hljs-keyword">transaction</span>
<span class="hljs-keyword">begin</span> try
  <span class="hljs-comment">-- insert new record into the orders table</span>
  <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> orders (orderid, customername, orderdate)
  <span class="hljs-keyword">values</span> (<span class="hljs-number">102</span>, <span class="hljs-string">'Jane Doe'</span>, <span class="hljs-keyword">getdate</span>())

  <span class="hljs-comment">-- simulate an error</span>
    <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> orderdetails (orderid, productid, quantity)
    <span class="hljs-keyword">values</span> (<span class="hljs-number">102</span>, <span class="hljs-literal">NULL</span>, <span class="hljs-number">5</span>)  <span class="hljs-comment">-- NULL is not allowed for ProductID</span>

    <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span>  <span class="hljs-comment">-- this will not run if an error occurs</span>
<span class="hljs-keyword">end</span> try
<span class="hljs-keyword">begin</span> catch
    <span class="hljs-keyword">select</span> <span class="hljs-string">'error. xact_abort automatically rolled back the transaction.'</span>
<span class="hljs-keyword">end</span> catch
</code></pre>
<p><strong>3. Error Logging and Reporting</strong><br /><strong>Logging Errors to a Table</strong><br />Storing error details in a log table helps with troubleshooting and debugging.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">begin</span> catch
  <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> errorlog (errormessage, errornumber, errorseverity, errorstate)
  <span class="hljs-keyword">values</span> (ERROR_MESSAGE(), ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE())
<span class="hljs-keyword">end</span> catch
</code></pre>
<p>It couldn't hurt to index the errorlog table for better query performance and to add other logging information such as SESSION_USER, APP_NAME(), HOST_NAME(), etc. to help make debugging easier and to add more context. If the error occurs in a stored procedure, then adding ERROR_PROCEDURE() and ERROR_LINE() for good measure might also be helpful.</p>
<p>Using Output Parameters for Error Reporting<br />Stored Procedures can return error messages via an output parameter.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">drop</span> <span class="hljs-keyword">procedure</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> dbo.MyProcedure
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">create</span> <span class="hljs-keyword">procedure</span> dbo.MyProcedure @ErrorMessage <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">4000</span>) <span class="hljs-keyword">output</span>
<span class="hljs-keyword">as</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">begin</span> try
    <span class="hljs-comment">-- sql logic</span>
  <span class="hljs-keyword">end</span> try
  <span class="hljs-keyword">begin</span> catch
    <span class="hljs-keyword">set</span> @errormessage = error_message() <span class="hljs-comment">-- return the error message to the calling code</span>
  <span class="hljs-keyword">end</span> catch
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">go</span>
</code></pre>
<p><strong>Conclusion</strong><br />Using SQL error handling techniques ensures database reliability and maintainability. Using TRY...CATCH, transaction management, and logging to a table can help improve system resilience, handle unexpected situations effectively and maintain system integrity.</p>
]]></content:encoded></item><item><title><![CDATA[Centralizing SQL Connection Handling with a DatabaseHelper Class]]></title><description><![CDATA[For applications that rely on SQL Server connectivity for real-time data processing and high-volume data operations, transient connection failures can occur due to network issues, server load, timeouts, etc. Implementing a custom SQL connection retry...]]></description><link>https://blog.seandrew.info/centralizing-sql-connection-handling-with-a-databasehelper-class</link><guid isPermaLink="true">https://blog.seandrew.info/centralizing-sql-connection-handling-with-a-databasehelper-class</guid><category><![CDATA[ASP.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[SQL Server]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Tue, 11 Feb 2025 20:38:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739306233591/ef8427fd-9a97-451a-97c8-56709d6cac4a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For applications that rely on SQL Server connectivity for real-time data processing and high-volume data operations, transient connection failures can occur due to network issues, server load, timeouts, etc. Implementing a custom SQL connection retry mechanism ensures that these temporary disruptions do not compromise application stability.</p>
<p>In this write-up, I look at a custom GetResilientConnection method in <a target="_blank" href="http://ADO.NET">ADO.NET</a> when connecting to SQL Server using SqlClient to ensure my application handles connection disruptions gracefully by implementing automated custom retry logic that allow my applications to attempt reconnections with controlled delays and limits.</p>
<p>Transient Connection Failures<br />These faults are temporary connectivity issues, often due to network instability or database server connectivity issues. While these errors are typically resolved once the network or server recovers, they can disrupt an application's flow unless handled properly. Some general transient connection examples can include:</p>
<p>• Network-related errors (e.g., error code 40613)<br />• SQL Server unavailability (e.g., error code 10928)</p>
<p><strong>Implementing Custom Retry Logic in</strong> <a target="_blank" href="http://ADO.NET"><strong>ADO.NET</strong></a><br />A simple yet effective way to enhance resilience is by implementing retry logic. When a transient error occurs, the application should wait for a brief period before attempting to reconnect. This code defines a class named DatabaseHelper, which provides a method called GetResilientConnection. This method is responsible for handling transient database connection failures by implementing a retry mechanism.</p>
<p>Sample Retry Pattern Code Example</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> System;
<span class="hljs-keyword">using</span> System.Data.SqlClient;
<span class="hljs-keyword">using</span> System.IO;
<span class="hljs-keyword">using</span> System.Linq;
<span class="hljs-keyword">using</span> System.Net.Sockets;
<span class="hljs-keyword">using</span> System.Threading;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DatabaseHelper</span>
{
  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> SqlConnection <span class="hljs-title">GetResilientConnection</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> connectionString</span>)</span>
  {
    <span class="hljs-keyword">int</span> maxRetries = <span class="hljs-number">3</span>;  <span class="hljs-comment">// maximum retry attempts</span>
    <span class="hljs-keyword">int</span> delay = <span class="hljs-number">2000</span>;    <span class="hljs-comment">// initial delay (2 seconds)</span>

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i &lt; maxRetries; i++)
    {
      <span class="hljs-keyword">try</span>
      {
        <span class="hljs-comment">// attempt to establish a SQL connection</span>
        <span class="hljs-keyword">var</span> connection = <span class="hljs-keyword">new</span> SqlConnection(connectionString);
        connection.Open(); <span class="hljs-comment">// open the connection</span>
        <span class="hljs-keyword">return</span> connection; <span class="hljs-comment">// successfully connected, return the connection</span>
      }
      <span class="hljs-keyword">catch</span> (Exception ex) <span class="hljs-keyword">when</span> (IsTransientError(ex)) <span class="hljs-comment">// catch broader transient errors</span>
      {
        Console.WriteLine(<span class="hljs-string">$"Transient error encountered: <span class="hljs-subst">{ex.Message}</span>"</span>);
        Console.WriteLine(<span class="hljs-string">$"Retrying in <span class="hljs-subst">{delay / <span class="hljs-number">1000</span>}</span> seconds..."</span>);

        Thread.Sleep(delay); <span class="hljs-comment">// wait before retrying (exponential backoff)</span>
        delay *= <span class="hljs-number">2</span>; <span class="hljs-comment">// double the delay for the next retry</span>
      }
    }

    <span class="hljs-comment">// if all retries fail, throw an exception indicating failure</span>
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Exception(<span class="hljs-string">"Max retry attempts exceeded."</span>);
  }

  <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">IsTransientError</span>(<span class="hljs-params">Exception ex</span>)</span>
  {
    <span class="hljs-comment">// some common SQL server transient error codes</span>
    <span class="hljs-keyword">int</span>[] transientErrors = { <span class="hljs-number">40613</span>, <span class="hljs-number">10928</span>, <span class="hljs-number">40197</span>, <span class="hljs-number">40501</span>, <span class="hljs-number">49918</span>, <span class="hljs-number">1205</span>, <span class="hljs-number">233</span>, <span class="hljs-number">-2</span> };

    <span class="hljs-keyword">if</span> (ex <span class="hljs-keyword">is</span> SqlException sqlEx)
    {
      <span class="hljs-comment">// check if any of the SQL error numbers match the transientErrors errors array</span>
      <span class="hljs-keyword">if</span> (sqlEx.Errors.Cast&lt;SqlError&gt;().Any(e =&gt; transientErrors.Contains(e.Number)))
      {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
      }
    }

    <span class="hljs-comment">// none of the erros codes in the transientErrors errors array were found</span>
    <span class="hljs-comment">// so look at these other types </span>
    <span class="hljs-comment">// additional transient errors based on exception types</span>
    <span class="hljs-keyword">return</span> ex <span class="hljs-keyword">is</span> TimeoutException  <span class="hljs-comment">// SQL command timeout</span>
      || ex <span class="hljs-keyword">is</span> IOException       <span class="hljs-comment">// general network failure</span>
      || ex <span class="hljs-keyword">is</span> InvalidOperationException <span class="hljs-comment">// connection unexpectedly closed</span>
      || ex <span class="hljs-keyword">is</span> SocketException; <span class="hljs-comment">// low-level socket failure</span>
  }
}
</code></pre>
<p>This GetResilientConnection example method does the following:<br />• Attempts to establish a SQL Server connection using the provided connection string.<br />• Retries up to 3 times if a transient failure occurs, with an increasing wait time between attempts - waits (2s → 4s → 8s) before retrying which can improve application server recovery chances. Retries up to 3 times before throwing an exception.<br />• Uses IsTransientError to detect common transient errors, including SQL timeouts, deadlocks, and network issues. Detects timeouts, network failures, and connection drops.<br />• Logs the failure and waits before retrying, using exponential backoff (doubling the delay each time).<br />• Throws an exception if all retry attempts fail.<br />• Encapsulated in a reusable helper class and can be called from multiple parts of the application for consistent connection handling.</p>
<p><strong>Example of GetResilientConnection Usage</strong><br />Instead of rewriting SQL connectivity retry logic everywhere in my application where I need to make a SQL connection, I can simply call DatabaseHelper.GetResilientConnection. When another part of my application references DatabaseHelper, it is using this class as a utility to establish SQL connections with built-in resilience.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> connection = DatabaseHelper.GetResilientConnection(connectionString))
{
  Console.WriteLine(<span class="hljs-string">"Connected successfully!"</span>);

  <span class="hljs-comment">// use the connection here</span>
  <span class="hljs-comment">// and do your data processing stuff</span>
  <span class="hljs-comment">// in the context of the connection</span>
}
</code></pre>
<p>This approach promotes code maintainability and follows the Single Responsibility Principle (SRP) and ensures consistent handling of connection failures across my application.</p>
<p><strong>The Encapsulation Part</strong><br />I created the custom DatabaseHelper class to centralize database logic and avoid repeating connection code throughout my application. By keeping (encapsulating) this logic in one place, I improve code maintainability and make it easier to update connection handling in the future (only one place if I want to add functionality or fix bugs).</p>
<p>Doing this also ensures that database connection logic remains separate from the rest of my application logic, which helps with making the codebase cleaner and more modular.</p>
<p>Within this class, the IsTransientError method is marked private, meaning transient error detection is only accessible inside DatabaseHelper, which ensures that SQL connectivity error handling is consistent and can only be leveraged by referencing the DatabaseHelper class.</p>
<p>Basically - since DatabaseHelper.GetResilientConnection(connectionString) is public and static, any part of my application that needs a resilient SQL connection can simply call it. This prevents duplicate code and ensures consistent retry logic across the application.</p>
<p><strong>Why Use It</strong><br />• Improves database connection reliability in case of temporary issues.<br />• Ensures automatic retry instead of failing immediately.<br />• Centralized connection handling reduces redundant code across my application.</p>
<p><strong>Conclusion</strong><br />By creating and using GetResilientConnection, my application can gracefully handle transient failures and improve reliability without requiring manual intervention and ensures that application database connectivity remains stable(ish). Proper logging and exception handling within the retry mechanism further help diagnose persistent issues, making it a valuable strategy for robust database interactions.</p>
]]></content:encoded></item><item><title><![CDATA[Launching Executables and Switching Focus in Delphi]]></title><description><![CDATA[I am currently using Delphi to provide support, implement enhancements, and resolve bugs in legacy monolithic Windows applications. One key enhancement involved creating a launch pad to manage multiple pre-existing external executables, requiring sea...]]></description><link>https://blog.seandrew.info/launching-executables-and-switching-focus-in-delphi</link><guid isPermaLink="true">https://blog.seandrew.info/launching-executables-and-switching-focus-in-delphi</guid><category><![CDATA[Delphi]]></category><category><![CDATA[pascal]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Tue, 21 Jan 2025 16:42:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737477606628/33c417a6-91df-4ff5-aab3-eb4d12ea3fac.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I am currently using Delphi to provide support, implement enhancements, and resolve bugs in legacy monolithic Windows applications. One key enhancement involved creating a launch pad to manage multiple pre-existing external executables, requiring seamless interaction with external programs. Whether it’s launching secondary tools like an image importer or bringing an already running application to the foreground, Delphi offers a powerful suite of tools to facilitate these tasks. By leveraging the Windows API and Delphi’s advanced process management capabilities, I can ensure smooth and efficient integration with external applications.</p>
<p>In this write up, I show how to achieve this functionality using Windows API and Delphi code.</p>
<p><strong>Key Steps</strong><br />Check if the Process is Running: Use the "IsProcessRunning" function to determine if the target application is already running. This function scans the active processes and matches their executable names with the desired process name.</p>
<p>Find the Window Associated with the Process: If the process is running, use the "FindWindowByProcess" function to locate the window handle of the application.</p>
<p>Bring the Application to the Foreground: Once the window handle is retrieved, the "SetForegroundWindow" API is used to bring the application window to the front. If minimized, the "ShowWindow" API restores it.</p>
<p>Launch the Application if it is Not Running: If the application is not running, the "ShellExecuteEx" API launches it. After launching, the program waits for the application window to appear and then brings it to the foreground.</p>
<p> <br /><strong>Example Code</strong><br />The following code implements the steps for a button click event in my Delphi application. This serves as the core logic and relies on two supporting functions.</p>
<pre><code class="lang-csharp">procedure TForm1.btnMyButtonClientEvent(Sender: TObject);
<span class="hljs-keyword">var</span>
  ExecInfo: TShellExecuteInfo; <span class="hljs-comment">// structure to define execution parameters</span>
  Handle: HWND;               <span class="hljs-comment">// handle to the window of the external application</span>
begin
  <span class="hljs-comment">// check if the process "TheExternalProgram.exe" is already running</span>
  <span class="hljs-function"><span class="hljs-keyword">if</span> <span class="hljs-title">IsProcessRunning</span>(<span class="hljs-params"><span class="hljs-string">'TheExternalProgram.exe'</span></span>) then
  begin
    <span class="hljs-comment">// ff the process is running, find its window by its title</span>
    Handle :</span>= FindWindow(nil, <span class="hljs-string">'Extenal Program Window Title'</span>);
    <span class="hljs-keyword">if</span> Handle &lt;&gt; <span class="hljs-number">0</span> then
    begin
      <span class="hljs-comment">// ff the window is found, restore it if minimized</span>
      ShowWindow(Handle, SW_RESTORE);
      <span class="hljs-comment">// bring the application window to the foreground</span>
      SetForegroundWindow(Handle);
      end;
  end
  <span class="hljs-keyword">else</span>
  begin
    <span class="hljs-comment">// if the external executable is not running then prepare to launch it</span>
    ZeroMemory(@ExecInfo, SizeOf(ExecInfo)); <span class="hljs-comment">// clear the structure</span>
    ExecInfo.cbSize := SizeOf(ExecInfo);  <span class="hljs-comment">// set the size of the structure</span>
    ExecInfo.fMask := SEE_MASK_NOCLOSEPROCESS; <span class="hljs-comment">// ensure process handle remains open after launch</span>
    ExecInfo.lpFile := <span class="hljs-string">'c:\thepath\TheExternalProgram.exe'</span>; <span class="hljs-comment">// path to the executable</span>
    ExecInfo.nShow := SW_SHOWNORMAL; <span class="hljs-comment">// show the application normally</span>

    <span class="hljs-comment">// attempt to launch the executable</span>
    <span class="hljs-function"><span class="hljs-keyword">if</span> <span class="hljs-title">ShellExecuteEx</span>(<span class="hljs-params">@ExecInfo</span>) then
    begin
      <span class="hljs-comment">// wait for the external exe main window to appear</span>
      repeat
        Handle :</span>= FindWindow(nil, <span class="hljs-string">'Extenal Program Window Title'</span>);
        Sleep(<span class="hljs-number">100</span>); <span class="hljs-comment">// pause for 100ms to allow the external exe to initialize</span>
      until (Handle &lt;&gt; <span class="hljs-number">0</span>); <span class="hljs-comment">// exit loop once the window handle is found</span>

      <span class="hljs-comment">// restore the external exe window if minimized and bring it to the foreground</span>
      ShowWindow(Handle, SW_RESTORE);
      SetForegroundWindow(Handle);
    end
    <span class="hljs-keyword">else</span>
    begin
      <span class="hljs-comment">// show an error message ff launching the external exe fails</span>
      MessageDlg(<span class="hljs-string">'Failed to launch TheExternalProgram.exe'</span>, mtInformation, [mbOk], <span class="hljs-number">0</span>);
    end;
  end;
end;
</code></pre>
<p><strong>Supporting Functions</strong><br />1. Checking if the Process is Running<br />This supporting code is for the "IsProcessRunning" function. This function checks whether a specific process, identified by its executable name, is currently running on the system. It takes a process name as input, takes a snapshot of all running processes, and compares each process name to the target. If it finds a match, the function returns True, indicating the process is running; otherwise, it returns False.</p>
<pre><code class="lang-csharp"><span class="hljs-function">function <span class="hljs-title">IsProcessRunning</span>(<span class="hljs-params"><span class="hljs-keyword">const</span> AProcessName: <span class="hljs-keyword">string</span></span>): Boolean</span>;
<span class="hljs-keyword">var</span>
  SnapShot: THandle; <span class="hljs-comment">// handle to the process snapshot</span>
  ProcessEntry: TProcessEntry32; <span class="hljs-comment">// structure to store process information</span>
begin
      Result := False; <span class="hljs-comment">// default to process not running</span>
  SnapShot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, <span class="hljs-number">0</span>); <span class="hljs-comment">// take a snapshot of all processes</span>
  <span class="hljs-keyword">if</span> SnapShot = INVALID_HANDLE_VALUE then Exit;

  ProcessEntry.dwSize := SizeOf(TProcessEntry32); <span class="hljs-comment">// initialize the structure size</span>

  <span class="hljs-function"><span class="hljs-keyword">if</span> <span class="hljs-title">Process32First</span>(<span class="hljs-params">SnapShot, ProcessEntry</span>) then <span class="hljs-comment">// iterate through the processes in the snapshot</span>
  begin
    repeat
      <span class="hljs-comment">// compare each process name with the target process name (case-insensitive)</span>
      <span class="hljs-keyword">if</span> <span class="hljs-title">SameText</span>(<span class="hljs-params">ProcessEntry.szExeFile, AProcessName</span>) then
      begin
        Result :</span>= True; <span class="hljs-comment">// process is running</span>
        Break;
      end;
    <span class="hljs-function">until not <span class="hljs-title">Process32Next</span>(<span class="hljs-params">SnapShot, ProcessEntry</span>)</span>; <span class="hljs-comment">// move to the next process</span>
  end;

  CloseHandle(SnapShot); <span class="hljs-comment">// release the snapshot handle</span>
end;
</code></pre>
<p>2. Finding the Window Associated with a Process<br />This supporting code is for the "FindWindowByProcess" function. This function searches for a window that is associated with a running process by matching the process name. It iterates through all processes and their windows, comparing the process ID of each window to the target process. If a match is found, it returns the handle of the corresponding window.</p>
<pre><code class="lang-csharp"><span class="hljs-function">function <span class="hljs-title">FindWindowByProcess</span>(<span class="hljs-params"><span class="hljs-keyword">const</span> ProcessName: <span class="hljs-keyword">string</span></span>): HWND</span>;
<span class="hljs-keyword">var</span>
  Snapshot: THandle; <span class="hljs-comment">// handle to the process snapshot</span>
  ProcessEntry: TProcessEntry32; <span class="hljs-comment">// Structure to store process information</span>
  Handle: HWND; <span class="hljs-comment">// handle to the window</span>
begin
  Result := <span class="hljs-number">0</span>; <span class="hljs-comment">// default to no window found</span>
  Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, <span class="hljs-number">0</span>); <span class="hljs-comment">// take a snapshot of all processes</span>
  <span class="hljs-keyword">if</span> Snapshot = INVALID_HANDLE_VALUE then Exit;

  ProcessEntry.dwSize := SizeOf(ProcessEntry); <span class="hljs-comment">// initialize the structure size</span>
  <span class="hljs-comment">// iterate through the processes in the snapshot</span>
  <span class="hljs-function"><span class="hljs-keyword">if</span> <span class="hljs-title">Process32First</span>(<span class="hljs-params">Snapshot, ProcessEntry</span>) then
  begin
    repeat
      <span class="hljs-comment">// check if the process name matches the target name (case-insensitive)</span>
      <span class="hljs-keyword">if</span> <span class="hljs-title">AnsiCompareText</span>(<span class="hljs-params">ProcessEntry.szExeFile, ProcessName</span>)</span> = <span class="hljs-number">0</span> then
      begin
        <span class="hljs-comment">// iterate through all top-level windows</span>
        Handle := FindWindow(nil, nil);
        <span class="hljs-keyword">while</span> Handle &lt;&gt; <span class="hljs-number">0</span> <span class="hljs-keyword">do</span>
        begin
          <span class="hljs-comment">// check if the window belongs to the target process</span>
          <span class="hljs-function"><span class="hljs-keyword">if</span> <span class="hljs-title">GetWindowThreadProcessId</span>(<span class="hljs-params">Handle, nil</span>)</span> = ProcessEntry.th32ProcessID then
          begin
            Result := Handle; <span class="hljs-comment">// found the window handle</span>
            Break;
          end;
          Handle := GetWindow(Handle, GW_HWNDNEXT); <span class="hljs-comment">// move to the next window</span>
        end;
        Break;
      end;
    <span class="hljs-function">until not <span class="hljs-title">Process32Next</span>(<span class="hljs-params">Snapshot, ProcessEntry</span>)</span>; <span class="hljs-comment">// move to the next process</span>
  end;

  CloseHandle(Snapshot); <span class="hljs-comment">// release the snapshot handle</span>
end;
</code></pre>
<p><strong>A Brief Note About the Order of Things</strong><br />In Delphi, the "IsProcessRunning" and "FindWindowByProcess" functions must be defined before the "btnMyButtonClick" event handler in the code. This order is essential because the button click event needs to reference these functions and they must be available at the time the event is triggered. By defining these functions earlier in my code, I ensure that the button click handler can successfully call them to check if the process is running and to find the associated window.</p>
<p><strong>Conclusion</strong><br />Incorporating external executables and managing their interactions within a Delphi application is essential when working with legacy systems. By leveraging Delphi’s integration with the Windows API and utilizing key API functions such as "ShellExecuteEx", "ShowWindow", and "SetForegroundWindow", I can easily check if the external program is running, bring its window to the foreground if necessary, and launch it if it’s not already active.</p>
]]></content:encoded></item><item><title><![CDATA[Automating Multi-Channel Client Notifications with a Custom Windows Service]]></title><description><![CDATA[My development of this Windows service stemmed from the need to automate and simplify client notifications, such as a notification about a prescription being ready for pickup, while ensuring both reliability and flexibility by seamlessly integrating ...]]></description><link>https://blog.seandrew.info/automating-multi-channel-client-notifications-with-a-custom-windows-service</link><guid isPermaLink="true">https://blog.seandrew.info/automating-multi-channel-client-notifications-with-a-custom-windows-service</guid><category><![CDATA[C#]]></category><category><![CDATA[windows service]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Fri, 17 Jan 2025 17:39:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737135423070/3afeed7b-1ae1-47a5-8103-7b0da048e5bb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My development of this Windows service stemmed from the need to automate and simplify client notifications, such as a notification about a prescription being ready for pickup, while ensuring both reliability and flexibility by seamlessly integrating with my custom API to process and deliver notifications via SMS, voice, and email using my Twilio based API endpoint.</p>
<p>By integrating with my custom API, the Windows service retrieves notification records from the database, validates the data, and triggers the appropriate API delivery method based on the message type (SMS, voice, email).</p>
<p>Note: <em>TheWinService</em> is a placeholder for the actual Windows background service name. I replaced the real name for clarity and confidentiality.</p>
<p>The Windows background service is composed of the following components:</p>
<p>App.config</p>
<ul>
<li>Stores application settings, connection strings, and configuration details, such as the API endpoint required for instantiation.</li>
</ul>
<p>Twilio.cs</p>
<ul>
<li>Includes static classes, general utility functions, and logic for retrieving records to process and handling general SQL logging.</li>
</ul>
<p>Program.cs</p>
<ul>
<li>Acts as the entry point for the Windows service. This file configures and initializes the service as a Windows background process, setting up essential dependencies like logging and the core service logic.</li>
</ul>
<p>WindowsBackgroundService.cs</p>
<ul>
<li>Implements the primary background task responsible for periodic processing of notifications, including messaging operations.</li>
</ul>
<p>App.config</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">configuration</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">appSettings</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">add</span> <span class="hljs-attr">key</span>=<span class="hljs-string">"service_timer_minutes"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"2"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">add</span> <span class="hljs-attr">key</span>=<span class="hljs-string">"RequestUri"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"https://the_base_url/Controller/APIEndpoint"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">add</span> <span class="hljs-attr">key</span>=<span class="hljs-string">"SQL"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"intergrated authentication and connection info"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">add</span> <span class="hljs-attr">key</span>=<span class="hljs-string">"HashKey"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"a randomly generated client specific hashkey"</span> /&gt;</span>

  <span class="hljs-tag">&lt;/<span class="hljs-name">appSettings</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">configuration</span>&gt;</span>
</code></pre>
<p>Twilio.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> System;
<span class="hljs-keyword">using</span> System.Collections.Generic;
<span class="hljs-keyword">using</span> System.Linq;
<span class="hljs-keyword">using</span> System.Text;
<span class="hljs-keyword">using</span> System.Threading.Tasks;
<span class="hljs-keyword">using</span> Twilio;
<span class="hljs-keyword">using</span> Twilio.TwiML;
<span class="hljs-keyword">using</span> Twilio.Rest.Api.V2010.Account;
<span class="hljs-keyword">using</span> Twilio.Types;
<span class="hljs-keyword">using</span> Twilio.AspNet.Core;
<span class="hljs-keyword">using</span> Twilio.AspNet.Common;
<span class="hljs-keyword">using</span> Microsoft.AspNetCore.Http;
<span class="hljs-keyword">using</span> <span class="hljs-keyword">static</span> Microsoft.Extensions.Logging.EventSource.LoggingEventSource;
<span class="hljs-keyword">using</span> Twilio.Http;
<span class="hljs-keyword">using</span> Twilio.TwiML.Messaging;
<span class="hljs-keyword">using</span> System.Reflection;
<span class="hljs-keyword">using</span> Newtonsoft.Json.Linq;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Logging;
<span class="hljs-keyword">using</span> System.Diagnostics;
<span class="hljs-keyword">using</span> <span class="hljs-keyword">static</span> TheWinService.GeneralFunctions;
<span class="hljs-keyword">using</span> <span class="hljs-keyword">static</span> TheWinService.WindowsBackgroundService;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">TheWinService</span>
{

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">TwilioRecordToProcess</span>
  {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? unique_patent_id  { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? to { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? <span class="hljs-keyword">from</span> { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? pkid { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? msgtype { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? msgtypevalue { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  }


  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">GeneralFunctions</span>
  {
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">IsNull</span>(<span class="hljs-params">[System.Diagnostics.CodeAnalysis.MaybeNullWhen(<span class="hljs-literal">true</span></span>)] <span class="hljs-keyword">object</span>? obj)</span> =&gt; obj == <span class="hljs-literal">null</span>;
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">bool</span> <span class="hljs-title">IsNotNull</span>(<span class="hljs-params">[System.Diagnostics.CodeAnalysis.NotNullWhen(<span class="hljs-literal">true</span></span>)] <span class="hljs-keyword">object</span>? obj)</span> =&gt; obj != <span class="hljs-literal">null</span>;


    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Globals</span>
    {
      <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span>? _sql_con_str = System.Configuration.ConfigurationManager.AppSettings[<span class="hljs-string">"SQL"</span>].Replace(<span class="hljs-string">@"\\\\"</span>, <span class="hljs-string">@"\\"</span>);
      <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span>? hashkey = System.Configuration.ConfigurationManager.AppSettings[<span class="hljs-string">"HashKey"</span>];
      <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">string</span>? RequestUri = System.Configuration.ConfigurationManager.AppSettings[<span class="hljs-string">"RequestUri"</span>];
    }


    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> TwilioRecordToProcess <span class="hljs-title">GetRecordToProcess</span>(<span class="hljs-params"></span>)</span>
    {
      <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"GetRecordToProcess"</span>); }

        TwilioRecordToProcess _TwilInfo = <span class="hljs-keyword">new</span>();

      <span class="hljs-keyword">try</span>
       {
         <span class="hljs-keyword">string</span>? _sql_con_str = Globals._sql_con_str;
         System.Data.SqlClient.SqlConnection _sql_con = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlConnection(_sql_con_str);
         System.Data.SqlClient.SqlCommand _sql_cmd = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlCommand(<span class="hljs-string">"GetTheRecToProcess "</span>, _sql_con);
         _sql_cmd.CommandType = System.Data.CommandType.StoredProcedure;
         _sql_con.Open();
         System.Data.SqlClient.SqlDataReader _sql_dr = _sql_cmd.ExecuteReader();
         <span class="hljs-keyword">while</span> (_sql_dr.Read())
         {
                  _TwilInfo.to = (<span class="hljs-keyword">string</span>?)_sql_dr[<span class="hljs-string">"destination"</span>];
                  _TwilInfo.unique_patent_id = (<span class="hljs-keyword">string</span>?)_sql_dr[<span class="hljs-string">"unique_patent_id"</span>].ToString();
                  _TwilInfo.<span class="hljs-keyword">from</span> = (<span class="hljs-keyword">string</span>?)_sql_dr[<span class="hljs-string">"twilio_from_number"</span>];
                  _TwilInfo.pkid = (<span class="hljs-keyword">string</span>?)_sql_dr[<span class="hljs-string">"pkid"</span>].ToString();
                  _TwilInfo.msgtype = (<span class="hljs-keyword">string</span>?)_sql_dr[<span class="hljs-string">"msgtype"</span>].ToString();
                  _TwilInfo.msgtypevalue = (<span class="hljs-keyword">string</span>?)_sql_dr[<span class="hljs-string">"msgtypevalue"</span>].ToString();
         }
         _sql_dr.Close();
         _sql_con.Close();
       }
       <span class="hljs-keyword">catch</span> (Exception ex)
       {
        <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows())
        {
          EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"GetRecordToProcess - ERROR"</span>);
          EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">$"GetRecordToProcess - ERROR: <span class="hljs-subst">{ex.Message.ToString()}</span>"</span>);
        }
       }

      <span class="hljs-keyword">return</span> _TwilInfo;
    }


    <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">LogEndpointCall</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> endpoint_name, <span class="hljs-keyword">string</span> payload, <span class="hljs-keyword">string</span> ParticipantMessagingBinding</span>)</span>
    {
      <span class="hljs-keyword">if</span> (IsNull(payload)) { payload = <span class="hljs-string">""</span>; }

      <span class="hljs-keyword">try</span>
      {
        <span class="hljs-keyword">string</span>? _sql_con_str = Globals._sql_con_str;
        System.Data.SqlClient.SqlConnection _sql_con = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlConnection(_sql_con_str);
        System.Data.SqlClient.SqlCommand _sql_cmd = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlCommand(<span class="hljs-string">"DoTheSQLLogging"</span>, _sql_con);
        _sql_cmd.CommandType = System.Data.CommandType.StoredProcedure;
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@p_partner_id"</span>, <span class="hljs-number">0</span>);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@p_endpoint_name"</span>, endpoint_name);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@p_payload"</span>, payload);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@p_ParticipantMessagingBinding"</span>, ParticipantMessagingBinding);
        _sql_con.Open();
        _sql_cmd.ExecuteNonQuery();
        _sql_con.Close();
      }
      <span class="hljs-keyword">catch</span> (Exception ex)
      {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Exception(ex.Message.ToString());
      }

      <span class="hljs-keyword">return</span> <span class="hljs-string">"logged"</span>;
    }
  }
}
</code></pre>
<p>Program.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> TheWinService;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Logging.Configuration;
<span class="hljs-keyword">using</span> Microsoft.Extensions.Logging.EventLog;

IHostBuilder builder = Host.CreateDefaultBuilder(args)
  .UseWindowsService(options =&gt;
  {
    options.ServiceName = <span class="hljs-string">"TheWinService"</span>;
  })
  .ConfigureServices((context, services) =&gt;
  {
    LoggerProviderOptions.RegisterProviderOptions&lt;
      EventLogSettings, EventLogLoggerProvider&gt;(services);

      services.AddSingleton&lt;CPTwilio&gt;();
      services.AddHostedService&lt;WindowsBackgroundService&gt;();

      services.AddLogging(builder =&gt;
      {
          builder.AddConfiguration(
              context.Configuration.GetSection(<span class="hljs-string">"Logging"</span>));
      });
  });

IHost host = builder.Build();
host.Run();
</code></pre>
<p>WindowsBackgroundService.cs</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> System.Diagnostics;
<span class="hljs-keyword">using</span> System.Security.Cryptography.Xml;
<span class="hljs-keyword">using</span> System.Security.Cryptography;
<span class="hljs-keyword">using</span> System.Text;
<span class="hljs-keyword">using</span> System.Net.Http;
<span class="hljs-keyword">using</span> System.Net;
<span class="hljs-keyword">using</span> Newtonsoft.Json;
<span class="hljs-keyword">using</span> System;
<span class="hljs-keyword">using</span> Microsoft.AspNetCore.Mvc;
<span class="hljs-keyword">using</span> Newtonsoft.Json.Linq;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">TheWinService</span>  ;


<span class="hljs-keyword">public</span> <span class="hljs-keyword">sealed</span> <span class="hljs-keyword">class</span> <span class="hljs-title">WindowsBackgroundService</span> : <span class="hljs-title">BackgroundService</span>
{
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> CPTwilio _TheWinService; <span class="hljs-comment">// used in IsCancellationRequested</span>
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> ILogger&lt;WindowsBackgroundService&gt; _logger; <span class="hljs-comment">// windows log</span>

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">SendTwilioSMSBody</span>
  {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? to { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? unique_patent_id    { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? <span class="hljs-keyword">from</span> { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  }


  <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">WindowsBackgroundService</span>(<span class="hljs-params">
    CPTwilio TheWinService  ,
    ILogger&lt;WindowsBackgroundService&gt; logger</span>)</span> =&gt;
    (_TheWinService  , _logger) = (TheWinService  , logger);


  <span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">async</span> Task <span class="hljs-title">ExecuteAsync</span>(<span class="hljs-params">CancellationToken stoppingToken</span>)</span>
  {
    <span class="hljs-keyword">try</span>
    {
      <span class="hljs-keyword">try</span>
      {
        <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"TheWinService   Start"</span>); }
      }
      <span class="hljs-keyword">catch</span> (Exception)
      { }


      <span class="hljs-keyword">while</span> (!stoppingToken.IsCancellationRequested)
      {
        <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"GetRecordToProcess"</span>); }

        <span class="hljs-keyword">dynamic</span> SendTwilioSMSBody = GeneralFunctions.GetRecordToProcess();

        GeneralFunctions.LogEndpointCall(<span class="hljs-string">"Begin ExecuteAsync TheWinService"</span>, <span class="hljs-string">""</span>, <span class="hljs-string">""</span>);

        <span class="hljs-keyword">if</span> (GeneralFunctions.IsNotNull(SendTwilioSMSBody.unique_patent_id  )) <span class="hljs-comment">// we must have a value</span>
        {
          <span class="hljs-keyword">var</span> ret_list = Newtonsoft.Json.JsonConvert.SerializeObject(SendTwilioSMSBody); <span class="hljs-comment">// convert class object into JSON</span>

          <span class="hljs-keyword">if</span> (!SendTwilioSMSBody.to.ToString().StartsWith(<span class="hljs-string">"+1"</span>))
          {
            <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"[to] must begin with +1 "</span> + ret_list); }
            GeneralFunctions.LogEndpointCall(<span class="hljs-string">"ExecuteAsync TheWinService"</span>, <span class="hljs-string">"[to] must begin with +1: "</span> + ret_list, <span class="hljs-string">""</span>);
            <span class="hljs-keyword">return</span>;
          }

          <span class="hljs-keyword">if</span> (SendTwilioSMSBody.to.ToString().Length != <span class="hljs-number">12</span>)
          {
            <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"[to] is incorrect length: "</span> + ret_list); }
            GeneralFunctions.LogEndpointCall(<span class="hljs-string">"ExecuteAsync TheWinService"</span>, <span class="hljs-string">"[to] is incorrect length: "</span> + ret_list, <span class="hljs-string">""</span>);
            <span class="hljs-keyword">return</span>;
          }
          <span class="hljs-keyword">string</span> ParticipantMessagingBinding = SendTwilioSMSBody.@from +<span class="hljs-string">"-"</span>+ SendTwilioSMSBody.@to;

          <span class="hljs-keyword">var</span> result = <span class="hljs-keyword">string</span>.Empty;
          <span class="hljs-keyword">string</span>? signature = <span class="hljs-keyword">string</span>.Empty;
          <span class="hljs-keyword">string</span>? timestamp = DateTime.Now.ToString(<span class="hljs-string">"yyyy'-'MM'-'dd 'T'HH':'mm':'ss'.'fff'Z'"</span>);

          <span class="hljs-keyword">byte</span>[] keyByte = Encoding.UTF8.GetBytes(TheWinService  .GeneralFunctions.Globals.hashkey!);
          <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> hmacsha256 = <span class="hljs-keyword">new</span> HMACSHA256(keyByte))
          {
            <span class="hljs-keyword">byte</span>[] textBytes = System.Text.Encoding.UTF8.GetBytes(<span class="hljs-string">$"<span class="hljs-subst">{timestamp}</span>:<span class="hljs-subst">{TheWinService  .GeneralFunctions.Globals.hashkey!}</span>"</span>);
            <span class="hljs-keyword">byte</span>[] hashBytes = hmacsha256.ComputeHash(textBytes);
            <span class="hljs-keyword">string</span> hash = BitConverter.ToString(hashBytes).Replace(<span class="hljs-string">"-"</span>, String.Empty);
            signature = hash.ToUpper();
          }

          <span class="hljs-comment">// the body stuff</span>
          <span class="hljs-keyword">var</span> content = <span class="hljs-keyword">new</span> StringContent(ret_list, Encoding.UTF8, <span class="hljs-string">"application/json"</span>); <span class="hljs-comment">// add payload to content as Body</span>
          content.Headers.Add(<span class="hljs-string">"cp_webhook_timestamp"</span>, timestamp);
          content.Headers.Add(<span class="hljs-string">"cp_webhook_signature"</span>, signature);

          HttpClient httpClient = <span class="hljs-keyword">new</span>();
          <span class="hljs-keyword">var</span> httpRequestMessage = <span class="hljs-keyword">new</span> HttpRequestMessage();
          httpRequestMessage.Method = HttpMethod.Post;

          <span class="hljs-comment">// add the content to the response</span>
          httpRequestMessage.Content = content; <span class="hljs-comment">// required for requestSignature == signature in PartnerRegisteredEndpoint</span>

          <span class="hljs-comment">// define the URI</span>
          <span class="hljs-keyword">string</span>? RequestUri = TheWinService  .GeneralFunctions.Globals.RequestUri!;
          httpRequestMessage.RequestUri = <span class="hljs-keyword">new</span> Uri(RequestUri!); <span class="hljs-comment">// set the partner_registered_endpoint</span>

          <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, RequestUri); }

          <span class="hljs-comment">// call the API TwilioEntryPoint endpoint</span>
          <span class="hljs-keyword">try</span>
          {
            <span class="hljs-keyword">var</span> response = <span class="hljs-keyword">await</span> httpClient.PostAsync(httpRequestMessage.RequestUri.ToString(), content);
            <span class="hljs-keyword">if</span> ((<span class="hljs-keyword">int</span>)response.StatusCode != <span class="hljs-number">200</span>)
            {
              <span class="hljs-keyword">string</span> resp = response.StatusCode.ToString() + <span class="hljs-string">" - "</span> + response.ToString() + <span class="hljs-string">" - "</span> + RequestUri;
              <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, resp); }
              <span class="hljs-keyword">return</span>;
            }
          }
          <span class="hljs-keyword">catch</span> (Exception)
          {
            <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"API endpoint did not respond."</span>); }
            <span class="hljs-keyword">return</span>;
          }

          <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"API SendTwilioSMS endpoint call complete"</span>); }
        }

        <span class="hljs-keyword">string</span>? _minute_timer = System.Configuration.ConfigurationManager.AppSettings[<span class="hljs-string">"service_timer_minutes"</span>];
        <span class="hljs-keyword">int</span> minutes = <span class="hljs-keyword">int</span>.Parse(_minute_timer!);
        <span class="hljs-keyword">await</span> Task.Delay(TimeSpan.FromMinutes(minutes), stoppingToken);
      }
    }
    <span class="hljs-keyword">catch</span> (TaskCanceledException)
    {
      <span class="hljs-comment">// When the stopping token is canceled, for example, a call made from services.msc,</span>
      <span class="hljs-comment">// we shouldn't exit with a non-zero exit code. In other words, this is expected...</span>
    }
    <span class="hljs-keyword">catch</span> (Exception ex)
    {
      _logger.LogError(ex, <span class="hljs-string">"{Message}"</span>, ex.Message); <span class="hljs-comment">// _logger is defined as WindowsBackgroundService</span>
      <span class="hljs-comment">// Terminates this process and returns an exit code to the operating system.</span>
      <span class="hljs-comment">// This is required to avoid the 'BackgroundServiceExceptionBehavior', which</span>
      <span class="hljs-comment">// performs one of two scenarios:</span>
      <span class="hljs-comment">// 1. When set to "Ignore": will do nothing at all, errors cause zombie services.</span>
      <span class="hljs-comment">// 2. When set to "StopHost": will cleanly stop the host, and log errors.</span>
      <span class="hljs-comment">//</span>
      <span class="hljs-comment">// In order for the Windows Service Management system to leverage configured</span>
      <span class="hljs-comment">// recovery options, we need to terminate the process with a non-zero exit code.</span>

      <span class="hljs-keyword">if</span> (OperatingSystem.IsWindows()) { EventLog.WriteEntry(<span class="hljs-string">"TheWinService"</span>, <span class="hljs-string">"TheWinService   Stop"</span>); }

      Environment.Exit(<span class="hljs-number">1</span>);
    }
  }
}
</code></pre>
<p><strong>Overview: Twilio Windows Background Service</strong><br />This Windows Background Service, implemented in C#, is designed to automate the delivery of notifications using Twilio. The service continuously monitors a SQL database for new records indicating messages to be sent, such as a notification about a prescription being ready for pickup. The key features and workflow of this service are as follows:</p>
<p><strong>General Features</strong></p>
<ul>
<li><p>Customizable Interval: The service polls the SQL database at a configurable interval (default: 2 minutes) to check for new records.</p>
</li>
<li><p>SQL Integration: Retrieves relevant data from the database for preparation.</p>
</li>
<li><p>Custom API Integration: Constructs the message body and initiates an endpoint call to my custom API which, based on message type, sends messages to recipients using Twilio integration.</p>
</li>
<li><p>Error Handling and Logging: Logs activity and results in both the SQL database and the Windows Event Log. Detects and logs common issues, such as invalid phone numbers or failed API calls.</p>
</li>
<li><p>Secure Communication: Generates secure signatures for API calls using HMAC-SHA256 to authenticate requests.</p>
</li>
<li><p>Graceful Shutdown: Handles task cancellations when the service is stopped, ensuring a clean exit.</p>
</li>
</ul>
<p><strong>General Workflow</strong><br />Initialization:</p>
<ul>
<li><p>The service starts and writes an entry to the Windows Event Log.</p>
</li>
<li><p>Retrieves the polling interval and endpoint configuration from app settings.</p>
</li>
</ul>
<p>Polling Loop:</p>
<ul>
<li><p>Queries the SQL database for a record indicating a message to process.</p>
</li>
<li><p>If a record exists:</p>
<ul>
<li><p>Serializes the record into JSON for use in the request payload.</p>
</li>
<li><p>Validates the recipient's phone number (e.g., must begin with +1 and be 12 characters long).</p>
</li>
</ul>
</li>
<li><p>Constructs the API request:</p>
<ul>
<li><p>Generates a timestamp and HMAC signature for secure communication.</p>
</li>
<li><p>Sets headers and body content for the HTTP request.</p>
</li>
<li><p>Communicates with the custom API endpoint to send message data using Twilio integration and handles the response:</p>
<ul>
<li>Logs any errors or issues with the API call to the Windows Event Log and database.</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Logging and Delays:</p>
<ul>
<li><p>Logs success or failure of each operation to the database and Windows Event Log.</p>
</li>
<li><p>Waits for the configured interval before polling the database again.</p>
</li>
</ul>
<p>Shutdown:</p>
<ul>
<li><p>Monitors for cancellation requests (e.g., service stop from Windows).</p>
</li>
<li><p>Performs cleanup tasks and ensures a clean exit.</p>
</li>
</ul>
<p><strong>Detailed Workflow</strong><br />1.Host Configuration:</p>
<ul>
<li><p>The Host.CreateDefaultBuilder initializes the default host for the application.</p>
</li>
<li><p>The UseWindowsService method sets up the application as a Windows Service and specifies its name.</p>
</li>
</ul>
<p>2.Service Registration:</p>
<ul>
<li><p>The ConfigureServices method is used to register required services and the hosted background service:</p>
<ul>
<li>WindowsBackgroundService: The main background task that performs periodic processing.</li>
</ul>
</li>
<li><p>Configures logging to use the Windows Event Log with customizable settings.</p>
</li>
</ul>
<p>3.Service Execution:</p>
<ul>
<li>The <a target="_blank" href="http://host.Run">host.Run</a>() method starts the service, transitioning it into the active state where it begins processing messaging tasks.</li>
</ul>
<p><strong>Key Features</strong><br />1.Windows Service Configuration:</p>
<ul>
<li><p>The service is configured to run as a Windows Service with the name "TheWinSerice".</p>
</li>
<li><p>This enables the service to integrate seamlessly with the Windows Service Manager. 2.Dependency Injection:</p>
</li>
<li><p>Adds the WindowsBackgroundService as a hosted service, which contains the core logic for Twilio message processing. 3.Logging:</p>
</li>
<li><p>Configures logging using the Windows Event Log.</p>
</li>
<li><p>Allows further customization by reading logging configurations from the application settings (appsettings.json). <a target="_blank" href="http://4.Host">4.Host</a> Creation:</p>
</li>
<li><p>Uses the Host.CreateDefaultBuilder method to set up the service's default environment, including configuration and dependency injection.</p>
</li>
<li><p>The UseWindowsService method specifies that this application is designed to run as a Windows Service.</p>
</li>
</ul>
<p><strong>Overview of the Integration and Workflow</strong><br />Windows Service for Orchestration: The Windows service acts as the primary orchestrator, continuously monitoring the database for new notifications. It operates at a configurable interval (e.g., every 2 minutes), fetching records that require processing. For each record:</p>
<ul>
<li><p>It validates the data.</p>
</li>
<li><p>Prepares the payload with details like recipient, message type, and content.</p>
</li>
<li><p>Sends the data to the TwilioEntryPoint API endpoint for further processing.</p>
</li>
</ul>
<p>Centralized API Endpoint: The custom API's "TwilioEntryPoint" endpoint serves as the hub for processing notifications. Upon receiving a request:</p>
<ul>
<li><p>It parses the payload to extract key information (e.g., recipient details, message type).</p>
</li>
<li><p>Based on the message type (msgtype), the API routes the request to the appropriate handler:</p>
<ul>
<li><p>SMS notifications are sent using Twilio's messaging services.</p>
</li>
<li><p>Voice notifications initiate a call using the Twilio Voice API.</p>
</li>
<li><p>Email notifications are processed via a dedicated email controller.</p>
</li>
</ul>
</li>
</ul>
<p>Global Configuration for Flexibility: A static global variable (RequestUri) stores the client-specific API entry endpoint. This ensures the solution can adapt to varying configurations across different deployments, allowing effortless customization without code changes.</p>
<p>Secure Communication: Each API call is signed with a unique HMACSHA256 signature and timestamp, ensuring secure and authenticated communication between the Windows service and the API.</p>
<p>Error Handling and Logging: Robust error handling and logging mechanisms are in place to ensure reliability:</p>
<ul>
<li><p>The Windows service logs errors and status updates to the Windows Event Log.</p>
</li>
<li><p>The API validates incoming requests, ensuring essential parameters are present and logging any discrepancies.</p>
</li>
</ul>
<p><strong>Benefits of the Approach</strong></p>
<ul>
<li><p>Scalability: Handles high volumes of notifications efficiently.</p>
</li>
<li><p>Flexibility: Easily adapts to client-specific configurations and requirements.</p>
</li>
<li><p>Reliability: Ensures secure, error-resistant communication between components.</p>
</li>
<li><p>Comprehensive Logging: Provides clear insights into the processing flow and status.</p>
</li>
</ul>
<p><strong>Conclusion</strong><br />The Program.cs file serves as the backbone of the messaging Windows Service, managing its initialization, configuration, and integration with the Windows Service infrastructure. By leveraging dependency injection and structured logging, it ensures modularity, maintainability, and ease of monitoring. Overall, this custom Twilio Windows service provides a robust solution for automating client notifications across multiple channels, including SMS, voice calls, and emails. By seamlessly integrating a custom API with database interactions and the Twilio API, the service guarantees efficient, reliable, and timely communication.</p>
]]></content:encoded></item><item><title><![CDATA[Dynamically Extracting Endpoint Names and Attributes from a Custom C# MVC API]]></title><description><![CDATA[Recently, I was asked whether a specific custom API endpoint functionality existed for a client. Unfortunately, I realized that I had been a bit lax in documenting the API endpoints as I added new functionality. In fact, more than a bit lax as the en...]]></description><link>https://blog.seandrew.info/dynamically-extracting-endpoint-names-and-attributes-from-a-custom-c-mvc-api</link><guid isPermaLink="true">https://blog.seandrew.info/dynamically-extracting-endpoint-names-and-attributes-from-a-custom-c-mvc-api</guid><category><![CDATA[api]]></category><category><![CDATA[C#]]></category><category><![CDATA[MVC architecture]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Wed, 15 Jan 2025 13:51:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736948788406/905f506f-40fa-4faa-bc84-d0595d4f4a66.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I was asked whether a specific custom API endpoint functionality existed for a client. Unfortunately, I realized that I had been a bit lax in documenting the API endpoints as I added new functionality. In fact, more than a bit lax as the endpoint documentation was practically non-existent (shhh, keep that a secret).</p>
<p>Rather than manually going through my code to extract each endpoint name, action, method, and route details (a process that is both time-consuming and error-prone), I decided to automate the task. My goal was to encapsulate the process within the custom API, keeping everything self-contained while creating a streamlined, dynamic way to generate API endpoint details.</p>
<p>Even before getting started, one of the first questions I considered was where to add all the necessary code within my existing Visual Studio project and which methodologies would be best suited to achieve this, which ultimately led to a significant amount of time researching the topic.</p>
<p>In this write-up, I walk through the process I followed to automate the extraction of endpoint names, actions, methods, and route details from one of my custom C# MVC APIs using Reflection and Dependency Injection.</p>
<p>Note: References to <em>MyCustomAPI</em> is a placeholder for the actual project name used. I replaced the real name for clarity and confidentiality.</p>
<p><strong>What is Reflection?</strong><br />It is a way for a program to "look at itself" while it's running. It is like a program looking in a mirror. It can see its own parts (like classes and methods) and use them, even if it did not know about them when it was written.</p>
<p><strong>What is Dependency Injection (DI)?</strong><br />It is like ordering parts for a machine. Instead of the machine finding its own parts, someone else gives it exactly what it needs to work. It is a way for managing how objects in a program get the things (dependencies/parts) they need to work.</p>
<p><strong>Overview of the Steps to Set This Up in My Existing Project</strong></p>
<ol>
<li><p>Create a new service named "EndpointMetadataService.cs".</p>
</li>
<li><p>Register the new service by adding code to Program.cs</p>
</li>
<li><p>Create a new controller named "DocsController.cs".</p>
</li>
<li><p>Create a new model named "EndpointMetadata.cs" to represent the endpoint metadata.</p>
</li>
</ol>
<p><strong>Step-by-Step Implementation</strong><br />1. Create a Service for Endpoint Extraction</p>
<p>I am using a dedicated service for endpoint extraction to keep the logic separate from other parts of the application (Separation of Concerns). This approach isolates the endpoint extraction logic from controllers and startup configurations, making it easier to maintain, reuse, and integrate.</p>
<p>A folder named "Services" did not exist in my project, so I created a new folder named "Services" in the root of the project (at the same level as "Controllers", "Models", and "Views").</p>
<p>I added a new blank class file to my project named "EndpointMetadataService.cs" in the new "Services" folder."EndpointMetadataService.cs" will be used to handle the reflection and endpoint metadata extraction.</p>
<p>This is the code for the "EndpointMetadataService.cs" Service.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> MyCustomAPI.Models; <span class="hljs-comment">// add reference to the model namespace</span>


<span class="hljs-keyword">namespace</span> <span class="hljs-title">MyCustomAPI.Services</span>
{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EndpointMetadataService</span>
  {
    <span class="hljs-comment">// method to retrieve metadata about all endpoints in the application</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> List&lt;EndpointMetadata&gt; <span class="hljs-title">GetEndpoints</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-comment">// Get the assembly that contains the executing code (current project)</span>
        <span class="hljs-keyword">var</span> assembly = Assembly.GetExecutingAssembly();

        <span class="hljs-comment">// Find all classes in the assembly that are derived from ControllerBase (i.e., are controllers)</span>
        <span class="hljs-comment">// Exclude abstract classes as they cannot be instantiated</span>
        <span class="hljs-keyword">var</span> controllers = assembly.GetTypes()
          .Where(type =&gt; <span class="hljs-keyword">typeof</span>(ControllerBase).IsAssignableFrom(type) &amp;&amp; !type.IsAbstract);

        <span class="hljs-comment">// Create metadata for each controller and its associated actions</span>
        <span class="hljs-keyword">var</span> endpoints = controllers.Select(controller =&gt; <span class="hljs-keyword">new</span> EndpointMetadata
        {
          Controller = controller.Name.Replace(<span class="hljs-string">"Controller"</span>, <span class="hljs-string">""</span>), <span class="hljs-comment">// remove the controller suffix</span>

          <span class="hljs-comment">// Retrieve all public instance methods that are action methods</span>
          Actions = controller.GetMethods(BindingFlags.Instance | BindingFlags.Public)
            <span class="hljs-comment">// Filter methods that have HTTP method attributes (e.g., [HttpGet], [HttpPost])</span>
            .Where(method =&gt; method.GetCustomAttributes&lt;HttpMethodAttribute&gt;().Any())
            .Select(method =&gt; <span class="hljs-keyword">new</span> EndpointAction
            {
              Action = method.Name, <span class="hljs-comment">// Name of the action method</span>
              <span class="hljs-comment">// HTTP methods associated with the action (e.g., GET, POST)</span>
              Methods = method.GetCustomAttributes&lt;HttpMethodAttribute&gt;().Select(attr =&gt; <span class="hljs-keyword">string</span>.Join(<span class="hljs-string">", "</span>, attr.HttpMethods)),
              <span class="hljs-comment">// Route templates defined for the action (e.g., "api/values/{id}")</span>
              Routes = method.GetCustomAttributes&lt;RouteAttribute&gt;().Select(attr =&gt; attr.Template ?? <span class="hljs-string">"[default]"</span>), <span class="hljs-comment">// Default to "[default]" if no route is specified</span>
              Description = method.GetCustomAttribute&lt;DescriptionAttribute&gt;()?.Description ?? <span class="hljs-string">"No description available"</span>,
              Parameters = method.GetParameters().Select(param =&gt; <span class="hljs-keyword">new</span> ParameterMetadata
              {
                Name = param.Name!,
                Type = param.ParameterType.Name,
                IsRequired = !param.HasDefaultValue
              }).ToList(), <span class="hljs-comment">// Convert actions to a list</span>
              ResponseType = method.ReturnType.Name
            }).ToList() <span class="hljs-comment">// Convert controllers to a list</span>
        }).ToList();

        <span class="hljs-comment">// Return the constructed list of endpoint metadata</span>
        <span class="hljs-keyword">return</span> endpoints;
    }

  }
}
</code></pre>
<p>2.Register the Service in Dependency Injection<br />I am using <a target="_blank" href="http://ASP.NET">ASP.NET</a> Core 6+, so I modified" Program.cs". .NET versions older than 6 would require you to make the changes in "Startup.cs".</p>
<p>I added this code to ensure that the "EndpointMetadataService" is available for use in all of the controllers and other parts of the API (Dependency Injection).<br /><code>// Register the EndpointMetadataService service</code> <a target="_blank" href="http://builder.Services"><code>builder.Services</code></a><code>.AddSingleton&lt;EndpointMetadataService&gt;();</code></p>
<p>This should be done before building and running the application with “builder.Build()” . I put the new code directly after this line of code.<br /><code>var builder = WebApplication.CreateBuilder(args);</code></p>
<p>3.Create a new Controller</p>
<p>I created a new Controller named "DocsController.cs" with an API endpoint <code>([HttpGet("endpoints")])</code> for exposing the endpoint metadata. I created this new Controller in the "Controller" folder of my project with a new "endpoints" endpoint.</p>
<p>This is the code for the "DocsController.cs" Controller.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">using</span> MyCustomAPI.Services;
<span class="hljs-keyword">using</span> Microsoft.AspNetCore.Mvc;

<span class="hljs-keyword">namespace</span> <span class="hljs-title">MyCustomAPI.Controllers</span>
{
  [<span class="hljs-meta">ApiController</span>]
  [<span class="hljs-meta">Route(<span class="hljs-meta-string">"api/[controller]"</span>)</span>]
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">DocsController</span> : <span class="hljs-title">ControllerBase</span>
  {
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> EndpointMetadataService _metadataService; <span class="hljs-comment">// Dependency to handle endpoint metadata</span>


    <span class="hljs-comment">// Constructor to inject the EndpointMetadataService dependency</span>
    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">DocsController</span>(<span class="hljs-params">EndpointMetadataService metadataService</span>)</span>
    {
      _metadataService = metadataService; <span class="hljs-comment">// Assign the injected service to a private readonly field</span>
    }


    <span class="hljs-comment">// Handles GET requests to "api/docs/endpoints".</span>
    <span class="hljs-comment">// Retrieves a list of endpoints metadata from the service and returns it in the HTTP response.</span>
    [<span class="hljs-meta">HttpGet(<span class="hljs-meta-string">"endpoints"</span>)</span>] <span class="hljs-comment">// basically the addressable endpoint name for the URL</span>
    [<span class="hljs-meta">Description(<span class="hljs-meta-string">"Retrieve each endpoint name, action, method, parameters, and route details"</span>)</span>]
    <span class="hljs-function"><span class="hljs-keyword">public</span> IActionResult <span class="hljs-title">GetEndpoints</span>(<span class="hljs-params"></span>)</span>
    {
      <span class="hljs-comment">// Fetches endpoint metadata</span>
      <span class="hljs-keyword">var</span> endpoints = _metadataService.GetEndpoints();

      <span class="hljs-comment">// return an OK response with the endpoints as the response body</span>
      <span class="hljs-keyword">return</span> Ok(endpoints);
    }
  }
}
</code></pre>
<p>4.Create a new model to represent the endpoint metadata</p>
<p>I created a new Model named "EndpointMetadata.cs" to represent the endpoint metadata. I created this new Model in the "Model" folder of my project.</p>
<p>The class definitions for the "EndpointMetadata.cs" Model.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">namespace</span> <span class="hljs-title">MyCustomAPI.Models</span>
{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EndpointMetadata</span>
  {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Controller { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> IEnumerable&lt;EndpointAction&gt;? Actions { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  }

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">EndpointAction</span>
  {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Action { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> IEnumerable&lt;<span class="hljs-keyword">string</span>&gt;? Methods { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> IEnumerable&lt;<span class="hljs-keyword">string</span>&gt;? Routes { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Description { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> List&lt;ParameterMetadata&gt;? Parameters { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? ResponseType { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  }

  <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">ParameterMetadata</span>
  {
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Name { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Type { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> IsRequired { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  }
}
</code></pre>
<p>The "EndpointMetadata" class represents the metadata for each controller with a list of "EndpointAction" objects that detail the actions (methods) as well as HTTP methods (verbs) and routes.</p>
<p>The "EndpointAction" class represents each action in the controller, including its name, HTTP methods, and routes.</p>
<p><strong>Accessing the new Endpoint</strong><br />To access the new "endpoints" endpoint in the DocsController.cs in development, I simply reference the following URL: http://localhost/api/docs/endpoints</p>
<p>The endpoint will return the dynamically extracted metadata of my API endpoints, including their names, actions, HTTP methods, and routes, and display that information in a JSON format, either in my browser or Postman, depending on which method I use.</p>
<p><strong>Conclusion</strong><br />Adding a description tag after the endpoint verb declaration can be a helpful way to describe the endpoint's purpose. For instance:</p>
<pre><code class="lang-csharp">    [<span class="hljs-meta">HttpPost</span>]
    [<span class="hljs-meta">Description(<span class="hljs-meta-string">"Description of this API endpoint"</span>)</span>]
    [<span class="hljs-meta">Route(<span class="hljs-meta-string">"{controller}/{action}/{variable1}/{variable2}"</span>)</span>]
</code></pre>
<p>Documentation is a critical part of application design and development, and in this case, I admittedly missed the mark. While there are other methods to achieve the functionality outlined in this write-up, my research led me to conclude that leveraging Reflection and Dependency Injection offers the most straightforward and easy-to-implement solution.</p>
<p>Automating the extraction of API endpoint names and attribute information in my application not only saves time but also ensures the data remains up-to-date with minimal manual effort. By leveraging Reflection and integrating it into my C# MVC API, I can dynamically extract endpoint details, ensuring accuracy, minimizing human error, and providing a scalable solution for managing API endpoint information.</p>
]]></content:encoded></item><item><title><![CDATA[File Comparison Made Easy: Detecting New and Changed Files with PowerShell]]></title><description><![CDATA[Recently, I was tasked with comparing two directory structures of two different project versions and to identify 'new files' and 'changed files' between the most current version of the project and the prior version of the project. The idea was to pro...]]></description><link>https://blog.seandrew.info/file-comparison-made-easy-detecting-new-and-changed-files-with-powershell</link><guid isPermaLink="true">https://blog.seandrew.info/file-comparison-made-easy-detecting-new-and-changed-files-with-powershell</guid><category><![CDATA[Powershell]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Thu, 09 Jan 2025 17:57:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736445391655/2bd17f3c-4495-4b74-89c0-55bc65c9afdf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I was tasked with comparing two directory structures of two different project versions and to identify 'new files' and 'changed files' between the most current version of the project and the prior version of the project. The idea was to produce a list of files that are in the current version of the project and not in the prior version of the project as well as any files associated with current version of the project that have been changed from the prior version of the project.</p>
<p><strong>The Script</strong></p>
<pre><code class="lang-powershell"><span class="hljs-variable">$folder1</span> = <span class="hljs-string">"c:\PathToFolder1"</span>
<span class="hljs-variable">$folder2</span> = <span class="hljs-string">"c:\PathToFolder2"</span>

<span class="hljs-comment"># Get file lists</span>
<span class="hljs-variable">$folder1files</span> = <span class="hljs-built_in">Get-ChildItem</span> <span class="hljs-literal">-Recurse</span> <span class="hljs-operator">-File</span> <span class="hljs-variable">$folder1</span>
<span class="hljs-variable">$folder2files</span> = <span class="hljs-built_in">Get-ChildItem</span> <span class="hljs-literal">-Recurse</span> <span class="hljs-operator">-File</span> <span class="hljs-variable">$folder2</span>

<span class="hljs-comment"># Get relative paths for comparison</span>
<span class="hljs-variable">$relativePathFolder1</span> = <span class="hljs-selector-tag">@</span>{ }
<span class="hljs-variable">$relativePathFolder2</span> = <span class="hljs-selector-tag">@</span>{ }

<span class="hljs-keyword">foreach</span> (<span class="hljs-variable">$file</span> <span class="hljs-keyword">in</span> <span class="hljs-variable">$folder1files</span>)
{
  <span class="hljs-variable">$relativePathFolder1</span>[<span class="hljs-variable">$file</span><span class="hljs-type">.FullName.Substring</span>(<span class="hljs-variable">$folder1</span><span class="hljs-type">.Length</span> + <span class="hljs-number">1</span>)] = <span class="hljs-variable">$file</span>.FullName
}
<span class="hljs-keyword">foreach</span> (<span class="hljs-variable">$file</span> <span class="hljs-keyword">in</span> <span class="hljs-variable">$folder2files</span>)
{
  <span class="hljs-variable">$relativePathFolder2</span>[<span class="hljs-variable">$file</span><span class="hljs-type">.FullName.Substring</span>(<span class="hljs-variable">$folder2</span><span class="hljs-type">.Length</span> + <span class="hljs-number">1</span>)] = <span class="hljs-variable">$file</span>.FullName
}

<span class="hljs-comment"># Find new files</span>
<span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"`nFinding new files..."</span>
<span class="hljs-variable">$newFiles</span> = <span class="hljs-variable">$relativePathFolder1</span>.Keys | <span class="hljs-built_in">Where-Object</span> { <span class="hljs-operator">-not</span> <span class="hljs-variable">$relativePathFolder2</span>.ContainsKey(<span class="hljs-variable">$_</span>) }

<span class="hljs-comment"># Find changed files</span>
<span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"`nFinding changed files..."</span>
<span class="hljs-variable">$changedFiles</span> = <span class="hljs-selector-tag">@</span>()
<span class="hljs-keyword">foreach</span> (<span class="hljs-variable">$file</span> <span class="hljs-keyword">in</span> <span class="hljs-variable">$relativePathFolder1</span>.Keys)
{
  <span class="hljs-keyword">if</span> (<span class="hljs-variable">$relativePathFolder2</span>.ContainsKey(<span class="hljs-variable">$file</span>))
  {
    <span class="hljs-comment"># Calculate hashes for both files</span>
    <span class="hljs-variable">$hashfolder1</span> = <span class="hljs-built_in">Get-FileHash</span> <span class="hljs-variable">$relativePathFolder1</span>[<span class="hljs-variable">$file</span>] <span class="hljs-literal">-Algorithm</span> MD5 | <span class="hljs-built_in">Select-Object</span> <span class="hljs-literal">-ExpandProperty</span> Hash
    <span class="hljs-variable">$hashfolder2</span> = <span class="hljs-built_in">Get-FileHash</span> <span class="hljs-variable">$relativePathFolder2</span>[<span class="hljs-variable">$file</span>] <span class="hljs-literal">-Algorithm</span> MD5 | <span class="hljs-built_in">Select-Object</span> <span class="hljs-literal">-ExpandProperty</span> Hash

    <span class="hljs-comment"># Compare hashes</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-variable">$hashfolder1</span> <span class="hljs-operator">-ne</span> <span class="hljs-variable">$hashfolder2</span>)
    {
      <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"Changed file: <span class="hljs-variable">$file</span>"</span>
      <span class="hljs-variable">$changedFiles</span> += <span class="hljs-variable">$file</span>
    }
  }
}

<span class="hljs-comment"># Output results</span>
<span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"`nNew Files:"</span>
<span class="hljs-keyword">if</span> (<span class="hljs-variable">$newFiles</span>.Count <span class="hljs-operator">-eq</span> <span class="hljs-number">0</span>)
{
  <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"No new files found."</span>
}
<span class="hljs-keyword">else</span>
{
  <span class="hljs-variable">$newFiles</span> | <span class="hljs-built_in">ForEach-Object</span> { <span class="hljs-built_in">Write-Host</span> <span class="hljs-variable">$_</span> }
}

<span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"`nChanged Files:"</span>
<span class="hljs-keyword">if</span> (<span class="hljs-variable">$changedFiles</span>.Count <span class="hljs-operator">-eq</span> <span class="hljs-number">0</span>)
{
  <span class="hljs-built_in">Write-Host</span> <span class="hljs-string">"No changed files found."</span>
}
<span class="hljs-keyword">else</span>
{
  <span class="hljs-variable">$changedFiles</span> | <span class="hljs-built_in">ForEach-Object</span> { <span class="hljs-built_in">Write-Host</span> <span class="hljs-variable">$_</span> }
}
</code></pre>
<p><strong>Steps Performed by the Script</strong><br />1 Define Folder Paths:</p>
<ul>
<li>The paths to the 'PathToFolder1' and 'PathToFolder1' folders are stored in $folder1 and $folder2.</li>
</ul>
<p>2 Retrieve File Lists:</p>
<ul>
<li>Get-ChildItem is used to recursively retrieve all files (including those in subdirectories) from each folder.</li>
</ul>
<p>3 Generate Relative Path Dictionaries:</p>
<ul>
<li><p>Two dictionaries, $relativePathsFolder1 and $relativePathsFolder2, are created:</p>
<ul>
<li><p>Keys are file paths relative to the root folder.</p>
</li>
<li><p>Values are the full file paths.</p>
</li>
</ul>
</li>
<li><p>This enables direct comparison of files regardless of their root folder paths.</p>
</li>
</ul>
<p>4 Identify New Files:</p>
<ul>
<li><p>Compares the keys (relative paths) in $relativePathsFolder1 against $relativePathsFolder2.</p>
</li>
<li><p>Files in 'PathToFolder1' that are not present in 'PathToFolder2' are added to $newFiles.</p>
</li>
</ul>
<p>5 Identify Changed Files:</p>
<ul>
<li><p>Iterates through the relative paths in $relativePathsFolder1 that also exist in $relativePathsFolder2.</p>
</li>
<li><p>Computes the hash of each file using Get-FileHash with the MD5 algorithm.</p>
</li>
<li><p>Compares the hashes of the corresponding files from both folders.</p>
</li>
<li><p>Files with mismatched hashes are added to $changedFiles.</p>
</li>
</ul>
<p>6 Output Results:</p>
<ul>
<li><p>Lists new files by displaying each entry in $newFiles. If no new files are found, it outputs "No new files found."</p>
</li>
<li><p>Lists changed files by displaying each entry in $changedFiles. If no changed files are found, it outputs "No changed files found."</p>
</li>
</ul>
<p><strong>Purpose of Using File Hashes</strong><br />1 Content-Based Comparison:</p>
<ul>
<li><p>Why: Comparing files based on content rather than metadata ensures you accurately detect differences. Files may have the same name, size, and timestamps but still differ in content.</p>
</li>
<li><p>How: The MD5 hash uniquely represents the file's content. If two files have different hashes, their content is not identical.</p>
</li>
</ul>
<p>2 Efficient and Reliable:</p>
<ul>
<li><p>File hashes allow for a quick and reliable comparison of file content without manually inspecting the files or relying on less reliable methods like file size.</p>
</li>
<li><p>MD5 is widely used for this type of integrity checking in scenarios where cryptographic security isn't a concern (as in this use case).</p>
</li>
</ul>
<p><strong>Breaking Down the Hash Command</strong></p>
<ul>
<li><p>Get-FileHash:</p>
<ul>
<li>Calculates a hash of the file content using the MD5 algorithm.</li>
</ul>
</li>
<li><p>Algorithm MD5:</p>
<ul>
<li><p>Using MD5 for its simplicity and speed. While it is not suitable for cryptographic purposes, it works perfectly for file comparison.</p>
</li>
<li><p>I could have used the SHA-256 algorithm for greater reliability, however with the speed trade-off, it was not required for this comparison.</p>
</li>
</ul>
</li>
<li><p>Select-Object -ExpandProperty Hash:</p>
<ul>
<li>Extracts only the Hash property of the Get-FileHash result, making it easier to compare directly.</li>
</ul>
</li>
</ul>
<p><strong>Why Not Use Metadata?</strong></p>
<ul>
<li><p>Using file properties like size or timestamps could lead to false positives or negatives:</p>
</li>
<li><p>A file may change content without changing its size.</p>
</li>
<li><p>Timestamps might not reliably reflect changes, especially when copied or modified under certain conditions.</p>
</li>
</ul>
<p><strong>Key Features:</strong></p>
<ul>
<li><p>Recursive Comparison: Handles files in subdirectories by comparing relative paths.</p>
</li>
<li><p>Content Comparison: Ensures changes are detected based on content, not just metadata, using hash comparison.</p>
</li>
<li><p>Informative Output: Provides clear feedback on newly added or changed files.</p>
</li>
</ul>
<p><strong>Conclusion</strong><br />This script provides a simple solution for comparing files between two directories. By identifying new files and detecting changed files based on their content, the script ensures that you have a clear understanding of differences between folder versions. Its use of relative paths and hash comparisons ensures accuracy, while its straightforward implementation makes it easy to adapt for other scenarios and maintain better control over your directory structures.</p>
]]></content:encoded></item><item><title><![CDATA[DevExpress - Simplifying Server-to-Client Data Transfer with ASPxCallback JSProperties]]></title><description><![CDATA[When building interactive ASP.NET web applications with DevExpress controls, it is common to encounter scenarios where you need to transfer data from the server to the client dynamically. The "JSProperties" property of the "ASPxCallback" control prov...]]></description><link>https://blog.seandrew.info/devexpress-simplifying-server-to-client-data-transfer-with-aspxcallback-jsproperties</link><guid isPermaLink="true">https://blog.seandrew.info/devexpress-simplifying-server-to-client-data-transfer-with-aspxcallback-jsproperties</guid><category><![CDATA[ASP.NET]]></category><category><![CDATA[DevExpress]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Mon, 06 Jan 2025 18:46:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1736188596996/8791edc6-d136-4ad7-ab48-929c0fc7def8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building interactive <a target="_blank" href="http://ASP.NET">ASP.NET</a> web applications with DevExpress controls, it is common to encounter scenarios where you need to transfer data from the server to the client dynamically. The "<code>JSProperties"</code> property of the "ASPxCallback" control provides an elegant solution for this requirement.</p>
<p><code>"JSProperties"</code> is a DevExpress specific server-side property that allows you to define custom key-value pairs that are then sent to your client-side JavaScript code as part of the "ASPxCallback" control callback response. These properties can be accessed in JavaScript, enabling seamless communication between server-side logic and client-side functionality, which is very useful for updating UI elements, displaying messages, or handling dynamic interactions without requiring a full page reload.</p>
<p><strong>Some Key Features</strong> <code>"JSProperties"</code></p>
<p><em>Flexible Data Transfer</em>: Enables passing custom data like strings, numbers, or even JSON objects between server and client.</p>
<p><em>Easy Integration</em>: The data is accessible via the callback event argument on the client-side using JavaScript.</p>
<p><em>Customizable Keys</em>: Keys must start with the "<strong>cp</strong>" prefix (e.g., cpMyProperty).</p>
<p><strong>Practical Use Cases</strong></p>
<p><em>Displaying Status Messages</em>: Inform users of the result of a server-side operation.</p>
<p><em>Updating UI Components Dynamically</em>: Pass calculated values or server-generated data to update client-side UI elements.</p>
<p><em>Passing Validation Results</em>: Validate user input server-side and relay feedback to the client.</p>
<p><em>In this example code, I show how</em> <code>JSProperties</code> <em>works and provide a very basic example.</em></p>
<p><strong>Using ASPxCallbackPanel with Custom Client-Server Interaction</strong></p>
<p>In this example usage, an "ASPxCallbackPanel" hosts an "ASPxButton" (named clibtn_test) and an "ASPxTextBox" (named cli_MyTextBox) within its "PanelContent" collection. The callback mechanism is defined using the "OnCallback" server-side event handler "MyCallbackPanel_Callback" and the "EndCallback" client-side event handler "CliMyCallbackPanel_EndCallback."</p>
<p><strong>Server-Side Callback Handling</strong></p>
<p>During the server-side callback (MyCallbackPanel_Callback), custom properties are assigned to the JSProperties collection based on the operation's outcome:</p>
<p><strong>Success Case</strong></p>
<p>CaseInfo_CallbackPanel.JSProperties["cp_status"] = "success";</p>
<p>CaseInfo_CallbackPanel.JSProperties["cp_message"] = "success";</p>
<p>On success, these properties indicate a successful operation with appropriate status and message.</p>
<p><strong>Error Case</strong></p>
<p>CaseInfo_CallbackPanel.JSProperties["cp_status"] = "error";</p>
<p>CaseInfo_CallbackPanel.JSProperties["cp_message"] = $"{ex.Message}";</p>
<p>On failure, the properties convey the error status and detailed exception message.</p>
<p><strong>Client-Side Interaction</strong></p>
<p>The "CliMyCallbackPanel_EndCallback" client-side handler processes the callback response by:</p>
<p>1. Setting the text of cli_MyTextBox to the value of cp_message (retrieved from the server).</p>
<p>2. Clearing any existing value of s.cp_function to reset its state.</p>
<p><strong>Key Benefits</strong></p>
<p>This sample highlights how "JSProperties" simplifies dynamic client-server communication in DevExpress "ASPxCallbackPanel". It allows passing detailed operational results, ensuring the UI updates responsively and informs the user of the operation's outcome.</p>
<p><strong>Example Usage</strong></p>
<p>Define the ASPxCallbackPanel in the <a target="_blank" href="http://ASP.NET">ASP.NET</a> Form</p>
<p>Define the "ASPxCallbackPanel" with an "ID", "ClientInstanceName", "OnCallback" server-side event, and both an "ASPxButton" with a "ClientSideEvents" definition and a client visible/client disabled "ASPxTextBox" in the panel collection content.</p>
<pre><code class="lang-basic">&lt;dx:ASPxCallbackPanel
ID=<span class="hljs-string">"CaseInfo_CallbackPanel"</span>
ClientInstanceName=<span class="hljs-string">"cliCaseInfo_CallbackPanel"</span>
runat=<span class="hljs-string">"server"</span>
ClientVisible=<span class="hljs-string">"true"</span>
OnCallback=<span class="hljs-string">"MyCallbackPanel_Callback"</span>&gt;
&lt;ClientSideEvents EndCallback=<span class="hljs-string">"CliMyCallbackPanel_EndCallback"</span> /&gt;
&lt;PanelCollection&gt;
  &lt;dx:PanelContent ID=<span class="hljs-string">"PanelContent2"</span> runat=<span class="hljs-string">"server"</span>&gt;
      &lt;table border=<span class="hljs-string">"0"</span>&gt;
      &lt;tr&gt;
      &lt;td&gt;
        &lt;dx:ASPxButton
        ID=<span class="hljs-string">"btn_test"</span>
        ClientInstanceName=<span class="hljs-string">"clibtn_test"</span>
        ClientVisible=<span class="hljs-string">"true"</span>
        AutoPostBack=<span class="hljs-string">"false"</span>
        UseSubmitBehavior=<span class="hljs-string">"false"</span>
        runat=<span class="hljs-string">"server"</span>
        Text=<span class="hljs-string">"click here"</span>&gt;
        &lt;ClientSideEvents Click=<span class="hljs-string">"function(s, e){ DoClick('button_click'); }"</span> /&gt;
        &lt;/dx:ASPxButton&gt;
      &lt;/td&gt;
      &lt;/tr&gt;

      &lt;tr&gt;
      &lt;td&gt;
        &lt;dx:ASPxTextBox
        ClientEnabled=<span class="hljs-string">"false"</span>
        ClientVisible=<span class="hljs-string">"true"</span>
        ID=<span class="hljs-string">"MyTextBox"</span>
        ClientInstanceName=<span class="hljs-string">"cli_MyTextBox"</span>
        runat=<span class="hljs-string">"server"</span>
        Text=<span class="hljs-string">""</span>&gt;
        &lt;/dx:ASPxTextBox&gt;
      &lt;/td&gt;
      &lt;/tr&gt;
      &lt;/table&gt;
  &lt;/dx:PanelContent&gt;
&lt;/PanelCollection&gt;
&lt;/dx:ASPxCallbackPanel&gt;
</code></pre>
<p>The Client-side JavaScript for the ASPxButton ClientSideEvents Click Event</p>
<p>This client-side JavaScript code is run when the user clicks on the "ASPxButton". If the "_user_option" variable equals 'button_click' then force a callback on the "cliCaseInfo_CallbackPanel" CallbackPanel, else do nothing.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Do_Click</span>(<span class="hljs-params">_user_option</span>)
</span>{
  <span class="hljs-keyword">if</span> (_user_option === <span class="hljs-string">'button_click'</span>)
  {
    <span class="hljs-comment">// force a callback on the CallbackPanel</span>
    cliCaseInfo_CallbackPanel.PerformCallback(<span class="hljs-string">'button_click'</span>);
  }
}
</code></pre>
<p>Server-Side C# Code</p>
<p>This server-side code is run when the "ASPxCallbackPanel" does a callback when initiated by the "ASPxButton" and the "Do_Click" function. This server-side code is defined in the "OnCallback" property of the "ASPxCallbackPanel".</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MyCallbackPanel_Callback</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, DevExpress.Web.CallbackEventArgsBase e</span>)</span>
{
  <span class="hljs-keyword">if</span> (Regex.IsMatch(e.Parameter, <span class="hljs-string">"button_click"</span>)) <span class="hljs-comment">// sent here from javascript Do_Click function</span>
  {
    <span class="hljs-keyword">try</span>
    {

      <span class="hljs-comment">// your server code here</span>
      <span class="hljs-comment">// do something here</span>

      <span class="hljs-comment">// assign "success" values to custom propertiesfor the client-side</span>
      CaseInfo_CallbackPanel.JSProperties[<span class="hljs-string">"cp_status"</span>] = <span class="hljs-string">"success"</span>;
      CaseInfo_CallbackPanel.JSProperties[<span class="hljs-string">"cp_message"</span>] = <span class="hljs-string">"success"</span>;
    }
    <span class="hljs-keyword">catch</span> (Exception ex)
    {
      <span class="hljs-comment">// assign "error" values to custom propertiesfor the client-side</span>
      CaseInfo_CallbackPanel.JSProperties[<span class="hljs-string">"cp_status"</span>] = <span class="hljs-string">"error"</span>;
      CaseInfo_CallbackPanel.JSProperties[<span class="hljs-string">"cp_message"</span>] = <span class="hljs-string">$"<span class="hljs-subst">{ex.Message}</span>"</span>;
    }
  }
  <span class="hljs-keyword">else</span>
  {
    <span class="hljs-comment">// assign "nothing to process" values to custom propertiesfor the client-side</span>
    <span class="hljs-comment">// this not required but is here as a just in case</span>
    CaseInfo_CallbackPanel.JSProperties[<span class="hljs-string">"cp_status"</span>] = <span class="hljs-string">"nothing to process"</span>;
    CaseInfo_CallbackPanel.JSProperties[<span class="hljs-string">"cp_message"</span>] = <span class="hljs-string">"nothing to process"</span>;
  }

  <span class="hljs-keyword">return</span>;
}
</code></pre>
<p>The Client-side JavaScript for the cliCaseInfo_CallbackPanel ClientSideEvents EndCallback Event</p>
<p>This client-side JavaScript is run after the "ASPxCallbackPanel" callback completes. You could interact with other client controls here, such as setting a text box value, checking a checkbox, etc. Here I am setting the "Text" value of the "cli_MyTextBox" "ASPxTextBox" and then deleting the "s.cp_function" value.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CliMyCallbackPanel_EndCallback</span>(<span class="hljs-params">s, e</span>)
</span>{
  <span class="hljs-keyword">var</span> message = s.cpMessage;
  <span class="hljs-keyword">var</span> status = s.cpStatus;

  <span class="hljs-keyword">if</span> (status === <span class="hljs-string">"Success"</span> || status === <span class="hljs-string">"nothing to process"</span>)
  {
    <span class="hljs-comment">// set text box to the value of message (s.cpMessage)</span>
    cli_MyTextBox.SetText(message);
  }
  <span class="hljs-keyword">else</span>
  {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Callback failed"</span>);
  }

  <span class="hljs-comment">// just doing a little clean up here</span>
  <span class="hljs-keyword">delete</span> (s.cp_function);
}
</code></pre>
<p><strong>Conclusi</strong><a target="_blank" href="https://dev.to/t/aspnet"><strong>on</strong>  
</a>The "<code>JSProperties"</code> feature of the "ASPxCallback" control is an easy method to bridge the gap between server-side logic and client-side interactions. Using the transfer of custom key-value pairs during callbacks helps with dynamic, user friendly, and responsive user interfaces without the overhead of full-page postbacks.</p>
<p>The "<code>JSProperties"</code> feature is handy with displaying server-side validation results, updating UI components dynamically, or passing custom messages to the client with its straightforward key-value structure.</p>
]]></content:encoded></item><item><title><![CDATA[DevExpress - Enhancing ASP.NET Web Forms with the ASPxGridView Control]]></title><description><![CDATA[The DevExpress "ASPxGridView" control is a server-side control that allows you to display data from a data source in a grid within an ASP.NET web form application. It displays data source fields and records as columns and rows in a table-based layout...]]></description><link>https://blog.seandrew.info/devexpress-enhancing-aspnet-web-forms-with-the-aspxgridview-control</link><guid isPermaLink="true">https://blog.seandrew.info/devexpress-enhancing-aspnet-web-forms-with-the-aspxgridview-control</guid><category><![CDATA[ASP.NET]]></category><category><![CDATA[C#]]></category><category><![CDATA[DevExpress]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Sat, 04 Jan 2025 13:58:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735998891595/07d82c28-813b-4171-af2a-97accf52816a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The DevExpress "ASPxGridView" control is a server-side control that allows you to display data from a data source in a grid within an <a target="_blank" href="http://ASP.NET">ASP.NET</a> web form application. It displays data source fields and records as columns and rows in a table-based layout.</p>
<p>"ASPxGridView" is a significant improvement over the standard "GridView" control provided by <a target="_blank" href="http://ASP.NET">ASP.NET</a> and offers more customization options, client-side capabilities (via JavaScript). Out of box, it provides some of the following additional features:</p>
<ul>
<li><p>Master-detail relationships</p>
</li>
<li><p>In-place editing (inline editing, pop-up editing, etc.)</p>
</li>
<li><p>Export to Excel, PDF, etc.</p>
</li>
<li><p>Customizable themes and styles</p>
</li>
<li><p>Hierarchical data representation</p>
</li>
<li><p>Client-side scripting support (JavaScript)</p>
</li>
<li><p>Sorting, paging, and filtering</p>
</li>
</ul>
<p>While I won't go into the details of all the additional features of the "ASPxGridView" control, I will demonstrate some of the basic functionality of the control, including events such as "OnAfterPerformCallback", "OnCustomCallback", "BeginCallback", "EndCallback", and "CustomButtonClick", as well as properties like "DataSourceID" and "CssClass".</p>
<p>For the sake of simplicity, the sample code in this document omits error checking, exception handling, logging, and other practices. It is intended for illustrative purposes only and does not necessarily reflect best coding practices.</p>
<p><strong>Defining and Using the ASPxGridView Control on an</strong> <a target="_blank" href="http://ASP.NET"><strong>ASP.NET</strong></a> <strong>Web Form</strong></p>
<pre><code class="lang-basic">&lt;dx:ASPxGridView
ID=<span class="hljs-string">"gridSPL"</span>
ClientInstanceName=<span class="hljs-string">"cligridSPL"</span>
runat=<span class="hljs-string">"server"</span>
DataSourceID=<span class="hljs-string">"gridSPLDS"</span>
KeyFieldName=<span class="hljs-string">"recid"</span>
EnableCallBacks=<span class="hljs-string">"true"</span>
CssClass=<span class="hljs-string">"MailMenuSearchBox"</span>
OnCustomCallback=<span class="hljs-string">"cligridSPL_CustomCallback"</span>
OnAfterPerformCallback=<span class="hljs-string">"cligridSPL_AfterCallback"</span>
OnHtmlRowPrepared=<span class="hljs-string">"gridSPL_HtmlRowPrepared"</span>
OnCellEditorInitialize=<span class="hljs-string">"gridSPL_CellEditorInitialize"</span>
OnRowInserting=<span class="hljs-string">"gridSPL_RowInserting"</span>
OnRowValidating=<span class="hljs-string">"gridSPL_RowValidate"</span>&gt;
&lt;Columns&gt;
  &lt;dx:gridviewdatatextcolumn FieldName=<span class="hljs-string">"recid"</span> Visible=<span class="hljs-string">"false"</span>&gt;&lt;/dx:gridviewdatatextcolumn&gt;
  &lt;dx:GridViewCommandColumn&gt;
    &lt;CustomButtons&gt;
      &lt;dx:GridViewCommandColumnCustomButton ID=<span class="hljs-string">"http_addnew"</span> Text=<span class="hljs-string">"New"</span>&gt;&lt;/dx:GridViewCommandColumnCustomButton&gt;
    &lt;/CustomButtons&gt;
    &lt;CellStyle CssClass=<span class="hljs-string">"pointer"</span>&gt;&lt;/CellStyle&gt;
  &lt;/dx:GridViewCommandColumn&gt;
&lt;/Columns&gt;
&lt;ClientSideEvents
EndCallback=<span class="hljs-string">"function(s, e){ cligridSPL_EndCallback(s, e); }"</span>
BeginCallback=<span class="hljs-string">"function(s, e){ cligridSPL_BeginCallback(s, e); }"</span>
CustomButtonClick=<span class="hljs-string">"function(s, e){ cligridSPL_CustomButtonClick(s, e); }"</span> /&gt;
&lt;/dx:ASPxGridView&gt;
</code></pre>
<p>An Overview of ASPxGridView Properties and Methods</p>
<ul>
<li><p>ID - The server-side reference to the ASPxGridView control.</p>
</li>
<li><p>ClientInstanceName - The client-side reference to the ASPxGridView control.</p>
</li>
<li><p>DataSourceID - Defines the SQL data source for the ASPxGridView control.</p>
</li>
<li><p>KeyFieldName - The primary record from the SQL data source result set.</p>
</li>
<li><p>EnableCallBacks - Tells the the ASPxGridView control to allow callbacks.</p>
</li>
<li><p>OnCustomCallback - The server-side event to run on callback.</p>
</li>
<li><p>OnAfterPerformCallback - The server-side event to run after the callback completes.</p>
</li>
<li><p>OnHtmlRowPrepared - The server-side event for each grid row within the ASPxGridView. Typically used to change the style settings of each row.</p>
</li>
<li><p>OnCellEditorInitialize - When the Grid switches a row to edit mode, it sends a callback request to the server to initialize cell editors. Typically used to customize an editor’s settings before it is shown in the edit cell.</p>
</li>
<li><p>OnRowInserting - The server-side event that occurs when a user tries to save a newly inserted row.</p>
</li>
<li><p>OnRowValidating - This server-side event is automatically raised when a row is about to be updated. Typically used to validate the new/updated data. You can also compare it to the original data (OldValues and NewValues property).</p>
</li>
<li><p>EndCallback - The client-side JavaScript event to run at the end of the ASPxGridView callback.</p>
</li>
<li><p>BeginCallback - The client-side JavaScript event to run at the beginning of the ASPxGridView callback. This runs before the server-side event.</p>
</li>
</ul>
<p><strong>Note:</strong> The "" are executed before the server-side events.</p>
<p>The CssClass Property<br />DevExpress components implement CssClass properties that allow you to assign CSS classes to the component and its elements.</p>
<p>Specify the external styles file.</p>
<pre><code class="lang-basic">&lt;html xmlns=<span class="hljs-string">"http://www.w3.org/1999/xhtml"</span>&gt;
&lt;head id=<span class="hljs-string">"Head1"</span> runat=<span class="hljs-string">"server"</span>&gt;
  &lt;title&gt;Your Web App Title&lt;/title&gt;
  &lt;link href=<span class="hljs-string">"styles/main.css"</span> rel=<span class="hljs-string">"stylesheet"</span> type=<span class="hljs-string">"text/css"</span> /&gt;
&lt;/head&gt;
</code></pre>
<p>The style entry in the "styles/main.css" file.</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.MailMenuSearchBox</span>
{
  <span class="hljs-attribute">cursor</span>: pointer;
  <span class="hljs-attribute">font-family</span>: <span class="hljs-string">'Open Sans'</span>, sans-serif;
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">14px</span>;
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f8f8f8</span>;
}
</code></pre>
<p>DataSourceID<br />The "asp:SqlDataSource" is a standard <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Form control that provides a simple way to interact with a database directly from a web page without needing explicit code to manage the data connection. It simplifies data retrieval, insertion, updating, and deletion operations by automating much of the database interaction. Typically used for simple data binding tasks. No code behind, simply configure the control declaratively. Enables rapid development, especially for applications that require basic data operations with minimal code.</p>
<pre><code class="lang-basic">&lt;asp:SqlDataSource
ID=<span class="hljs-string">"gridSPLDS"</span>
runat=<span class="hljs-string">"server"</span>
ProviderName=<span class="hljs-string">"System.Data.SqlClient"</span>
EnableCaching=<span class="hljs-string">"false"</span>
ConnectionString=<span class="hljs-string">"&lt;%$ ConnectionStrings:YourSecureConnString %&gt;"</span>
SelectCommand=<span class="hljs-string">"sp_GetAllrecords"</span>
SelectCommandType=<span class="hljs-string">"StoredProcedure"</span>
InsertCommand=<span class="hljs-string">"sp_AddNewRecord"</span>
InsertCommandType=<span class="hljs-string">"StoredProcedure"</span>&gt;
&lt;SelectParameters&gt;
  &lt;asp:Parameter <span class="hljs-keyword">Name</span>=<span class="hljs-string">"p_matternum"</span> Type=<span class="hljs-string">"string"</span> /&gt;
  &lt;asp:Parameter <span class="hljs-keyword">Name</span>=<span class="hljs-string">"p_type"</span> Type=<span class="hljs-string">"string"</span> /&gt;
&lt;/SelectParameters&gt;
&lt;InsertParameters&gt;
  &lt;asp:Parameter <span class="hljs-keyword">Name</span>=<span class="hljs-string">"p_matternum"</span> Type=<span class="hljs-string">"String"</span> /&gt;
  &lt;asp:Parameter <span class="hljs-keyword">Name</span>=<span class="hljs-string">"p_type"</span> Type=<span class="hljs-string">"String"</span> /&gt;
&lt;/InsertParameters&gt;
&lt;/asp:SqlDataSource&gt;
</code></pre>
<p>OnHtmlRowPrepared<br />This code will change the row backcolor/forecolor based on the value of the "Cost" field in the row. It can also be used for when the ASPxGridView is in edit mode, to hide a row of data based on your criteria, disable row editing based on your criteria, etc.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">gridSPL_HtmlRowPrepared</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, DevExpress.Web.ASPxGridViewTableRowEventArgs e</span>)</span>
{
  <span class="hljs-keyword">if</span> (e.RowType == DevExpress.Web.GridViewRowType.Data)
  {
    <span class="hljs-keyword">int</span> price = Convert.ToInt32(e.GetValue(<span class="hljs-string">"Cost"</span>));
    <span class="hljs-keyword">if</span> (price &lt; <span class="hljs-number">20</span>) {e.Row.BackColor = System.Drawing.Color.LightCyan};
    <span class="hljs-keyword">if</span> (price &gt; <span class="hljs-number">50</span>) {e.Row.ForeColor = System.Drawing.Color.DarkRed};
  }
}
</code></pre>
<p>CustomButtonClick<br />The "GridViewCommandColumnCustomButton" "http_addnew" button executes the client-side JavaScript "cligridSPL_CustomButtonClick" function when clicked. Each row contains an "http_addnew" button.</p>
<pre><code class="lang-csharp"><span class="hljs-function">function <span class="hljs-title">cligridSPL_CustomButtonClick</span>(<span class="hljs-params">s, e</span>)</span>
{
  <span class="hljs-keyword">if</span> (e.buttonID == <span class="hljs-string">'http_addnew'</span>)
  {
    cligridSPL.PerformCallback(<span class="hljs-string">'grid_add_new_row'</span>)
    e.processOnServer = <span class="hljs-literal">true</span>;
  }
}
</code></pre>
<p>Which forces a server-side callback on the "cligridSPL ClientInstanceName", which then executes the "cligridSPL_CustomCallback" server-side event. In this instance, the "cligridSPL_CustomCallback" server-side event puts the ASPxGridView into edit mode allowing the user to add a new row of data.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cligridSPL_CustomCallback</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, DevExpress.Web.ASPxGridViewCustomCallbackEventArgs e</span>)</span>
{
  <span class="hljs-keyword">if</span> (Regex.IsMatch(e.Parameters, <span class="hljs-string">"grid_add_new_row"</span>)) <span class="hljs-comment">// sent here from javascript cligridSPL_CustomButtonClick function</span>
  {
    gridSPL.AddNewRow();
    gridSPL.JSProperties[<span class="hljs-string">"cp_function"</span>] = <span class="hljs-string">"grid_add_new_row"</span>
  }
}
</code></pre>
<p>OnCellEditorInitialize<br />When an "ASPxGridView" row switches to edit mode, it sends a server-side callback request to the server to initialize cell editors. Typically used to customize the "ASPxGridView" editor settings before the column is shown in the edit cell. In this instance, it will make the "location" field the initial edit focus.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">gridSPL_CellEditorInitialize</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, ASPxGridViewEditorEventArgs e</span>)</span>
{
  <span class="hljs-keyword">if</span> (e.Column.FieldName == <span class="hljs-string">"location"</span>)
  {
    e.Editor.Focus(); <span class="hljs-comment">// set location field focus</span>
  }
}
</code></pre>
<p>OnRowValidating<br />This server-side code will get the value of the e.NewValues["location"] field and validate it in my CommonAppCode class. Basically, it just makes sure the new location is valid based on my criteria. If the location value is not valid then the "_SharePointvalidationPassed" global public variable is set to false and e.RowError = "Invalid URL"; event is raised.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">gridSPL_RowValidate</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, DevExpress.Web.Data.ASPxDataValidationEventArgs e</span>)</span>
{
  <span class="hljs-keyword">string</span> rowtovalidate = Convert.ToString(e.NewValues[<span class="hljs-string">"location"</span>]);
  <span class="hljs-keyword">if</span> (!CommonAppCode.ValidateUrl(rowtovalidate))
  {
    _SharePointvalidationPassed = <span class="hljs-literal">false</span>;
    e.RowError = <span class="hljs-string">"Invalid URL"</span>;
  }
}
</code></pre>
<p><strong>Note:</strong> "_SharePointvalidationPassed" is a global public variable that I pre-defined.<br /><code>public bool _SharePointvalidationPassed = true;</code></p>
<p>OnRowInserting<br />This server-side code sets the "p_casenum" field to the value of a form textbox control, "p_loctype" is set "S", and both "p_location" and "p_indexlocation" are set to the value of e.NewValues["location"] and the "gridSPL" ASPxGridView is rebound to its data source.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">gridSPL_RowInserting</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, DevExpress.Web.Data.ASPxDataInsertingEventArgs e</span>)</span>
{
  e.NewValues[<span class="hljs-string">"p_casenum"</span>] = txtbox10.Text; <span class="hljs-comment">// the text box value</span>
  e.NewValues[<span class="hljs-string">"p_loctype"</span>] = <span class="hljs-string">"S"</span>;
  e.NewValues[<span class="hljs-string">"p_location"</span>] = e.NewValues[<span class="hljs-string">"location"</span>];
  e.NewValues[<span class="hljs-string">"p_indexlocation"</span>] = e.NewValues[<span class="hljs-string">"location"</span>];
  gridSPL.DataBind();
}
</code></pre>
<p>BeginCallback<br />This JavaScript client-side code is run before the server-side "cligridSPL_CustomCallback" OnCustomCallback event is run. I am just showing this in case you would need to do some processing before running the OnCustomCallback server-side event. For example, If the user decided to cancel editing a row, you could cancel the edit without running the OnCustomCallback server-side event, which would save a server round trip.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cligridSPL_BeginCallback</span>(<span class="hljs-params">s, e</span>)
</span>{
  <span class="hljs-keyword">if</span> (e.command == <span class="hljs-string">'CANCELEDIT'</span>)
  {
    e.cancel =- <span class="hljs-literal">true</span>; <span class="hljs-comment">// cancel the edit</span>
  }
}
</code></pre>
<p>OnAfterPerformCallback<br />This server-side code run after the server-side "cligridSPL_CustomCallback" OnCustomCallback event is run. This code checks the "_SharePointvalidationPassed" global public variable that is set during validation in "gridSPL_RowValidate".</p>
<p>If it is "true" then pass "add_new_row_success" to the client-side "cligridSPL_EndCallback" javacript function for processing.</p>
<p>If it is "false" then pass "add_new_row_failed" to the client-side "cligridSPL_EndCallback" javacript function for processing.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">cligridSPL_AfterCallback</span>(<span class="hljs-params"><span class="hljs-keyword">object</span> sender, DevExpress.Web.ASPxGridViewAfterPerformCallbackEventArgs e</span>)</span>
{
  <span class="hljs-keyword">if</span> (_SharePointvalidationPassed) <span class="hljs-comment">// check variable set in gridSPL_RowValidate</span>
  {
    _SharePointvalidationPassed = <span class="hljs-literal">false</span>; <span class="hljs-comment">// reset global variable</span>
    gridSPL.JSProperties[<span class="hljs-string">"cp_function"</span>] = <span class="hljs-string">"add_new_row_success"</span>; <span class="hljs-comment">// pass this to the cligridSPL_EndCallback javacript function</span>
  }
  <span class="hljs-keyword">else</span>
  {
    gridSPL.JSProperties[<span class="hljs-string">"cp_function"</span>] = <span class="hljs-string">"add_new_row_failed"</span>; <span class="hljs-comment">// pass this to the cligridSPL_EndCallback javacript function</span>
  }
}
</code></pre>
<p>EndCallback<br />This JavaScript client-side code completes the callback processing. It checks the value of "s.cp_function" that is passed here from the "cligridSPL_CustomCallback" server-side event. It deletes the value of "s.cp_function" and then does some processing based on that value.</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">climainCallbackPanel_OnCallbackComplete</span>(<span class="hljs-params">s, e</span>)
</span>{
  <span class="hljs-keyword">switch</span> (s.cp_function)
  {
    <span class="hljs-keyword">case</span> <span class="hljs-string">'add_new_row_success'</span>:
      <span class="hljs-keyword">delete</span>(s.cp_function);
      <span class="hljs-comment">// display add new row success message to user</span>
      <span class="hljs-keyword">break</span>;

    <span class="hljs-keyword">case</span> <span class="hljs-string">'add_new_row_failed'</span>:
      <span class="hljs-keyword">delete</span>(s.cp_function);
      <span class="hljs-comment">// display add new row failed message to user</span>
      <span class="hljs-keyword">break</span>;

    <span class="hljs-keyword">default</span>:
      <span class="hljs-keyword">delete</span>(s.cp_function);
      <span class="hljs-keyword">break</span>;
  }
}
</code></pre>
<p><strong>Conclusion</strong><br />I have found the "ASPxGridView" control to be both easy to work with and a flexible solution for displaying and managing data in the interactive, data driven <a target="_blank" href="http://ASP.NET">ASP.NET</a> Web Forms applications I have developed over the years.</p>
]]></content:encoded></item><item><title><![CDATA[Working with Matter Team Membership Using the IntApp Walls API]]></title><description><![CDATA[The IntApp Walls API is a powerful tool for managing ethical walls and securely controlling access to sensitive data. By leveraging its operations, developers can efficiently interact with matter teams, manage memberships, and ensure compliance with ...]]></description><link>https://blog.seandrew.info/working-with-matter-team-membership-using-the-intapp-walls-api</link><guid isPermaLink="true">https://blog.seandrew.info/working-with-matter-team-membership-using-the-intapp-walls-api</guid><category><![CDATA[IntApp]]></category><category><![CDATA[C#]]></category><category><![CDATA[c#]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Wed, 01 Jan 2025 13:55:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735739533698/2d27034f-f3b6-4a62-aefd-d2f9c05cb2cf.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The IntApp Walls API is a powerful tool for managing ethical walls and securely controlling access to sensitive data. By leveraging its operations, developers can efficiently interact with matter teams, manage memberships, and ensure compliance with confidentiality requirements.</p>
<p>The Intapp Walls API is a SOAP web service that provides a programmatic interface for interacting with the Intapp Walls application. It is deployed as a standard component web service.</p>
<p>For the sake of simplicity, the sample code in this document omits error checking, exception handling, logging, and other practices. It is intended for illustrative purposes only and does not necessarily reflect best coding practices.</p>
<p>Here I walk through two key scenarios:</p>
<ol>
<li><p>Retrieving and listing matter team membership.</p>
</li>
<li><p>Adding a new member to an existing matter team.</p>
</li>
</ol>
<p>By understanding and using IntApp Walls API operations such as "GetMatterTeamForMatter", "LoadMatterTeam", and "AddUsersToMatterTeam", you can streamline tasks related to ethical wall management. The following examples include code snippets and step-by-step guidance.</p>
<p>This document will not cover the specifics of configuring development access to the IntApp Walls API. However, the management solution must be installed on your local domain, and the web service is typically accessible via a file named "APIService.svc", which should be added as a service reference in Visual Studio.</p>
<p><a target="_blank" href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8eutfo4ngcouyr60ml0.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu8eutfo4ngcouyr60ml0.png" alt="Image description" /></a></p>
<p>The sample code references the following IntApp Walls API operations:</p>
<ul>
<li><p>GetMatterTeamForMatter - Gets the ID of the matter team that is associated with the specified matter.</p>
</li>
<li><p>LoadMatterTeam - Loads the properties of a matter team.</p>
</li>
<li><p>GetDMSUserID - Get the DMS user ID. Some of the API methods require the DMS user ID for a user. For example, the CreateWall() method requires the user ID to be that of the DMS, not a user's timekeeper ID or records system ID. This method can be used to get the DMS user ID given another known ID for the user.</p>
</li>
<li><p>LoadMatterTeamMembership - Loads the matter team membership.</p>
</li>
<li><p>GetWarningsIfUserIsIncluded - Gets any warnings that would be generated if the specified user was granted access (i.e., included) to a particular client or matter. This function returns any warnings that may be generated by conflicting ethical walls.</p>
</li>
<li><p>AddUsersToMatterTeam - Adds the user to an existing matter team with a specified role.</p>
</li>
</ul>
<p><strong>Example: Retrieving and Listing Matter Team Membership</strong><br />The following code snippet uses the IntApp Walls API "GetMatterTeamForMatter" and "LoadMatterTeam" operations to retrieve a list of matter team members and then write the team membership particulars to the console.</p>
<p>Notes:<br />• Working with the IntApp API typically requires specific privileges, often granted to a service account with appropriate IntApp Walls access.<br />• References to "intapp_web_api" in the code snippet below, refers to the name of your IntApp API service reference as defined in Visual Studio.</p>
<p><a target="_blank" href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5de27rneo55wbb2x7pef.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5de27rneo55wbb2x7pef.png" alt="Image description" /></a></p>
<p>Step 1: Retrieve the unique IntApp Walls-managed matter team ID number.<br />Retrieve the ID of the matter team associated with a specified matter. This matter team ID will then be used to obtain the matter team membership details.</p>
<p>To achieve this, invoke the "GetMatterTeamForMatter" operation, which requires a "matterID" parameter. The "matterID" is typically an internally generated ID, sometimes referred to as a "case number." This value is supplied by the user or programmer from their own Timekeeper-type source.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">string</span> matterID = <span class="hljs-string">"01234"</span>; <span class="hljs-comment">// matterID supplied by you</span>
<span class="hljs-keyword">string</span> matterTeamID = String.Empty; <span class="hljs-comment">// the return value</span>

<span class="hljs-comment">// get the walls matter team id</span>
<span class="hljs-comment">// example of matter team id "COOLE-033517"</span>
matterTeamID = GetMatterTeamForMatter(matterID);

<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">GetMatterTeamForMatter</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> matterID</span>)</span>
{
  intapp_web_api.Matter matter = <span class="hljs-keyword">new</span> intapp_web_api.Matter();
  <span class="hljs-keyword">string</span> matterTeamID = <span class="hljs-keyword">string</span>.Empty;

  <span class="hljs-keyword">try</span>
  {
    intapp_web_api.APIServiceClient intapp_web_api = <span class="hljs-keyword">new</span> intapp_web_api.APIServiceClient();
    matterTeamID = intapp_web_api.GetMatterTeamForMatter(matterID);

    <span class="hljs-keyword">if</span> ((<span class="hljs-keyword">string</span>.IsNullOrEmpty(matterTeamID)))
    {
      matterTeamID = <span class="hljs-string">"blank"</span>;
    }
  }
  <span class="hljs-keyword">catch</span> (Exception ex)
  {
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrEmpty(matterTeamID) || ex.Message == <span class="hljs-string">"Error"</span>)
    {
      matterTeamID = <span class="hljs-string">"blank"</span>;
    }
  }
  <span class="hljs-keyword">return</span> matterTeamID;
}
</code></pre>
<p>Step 2: Load the Matter Team Results<br />Define the "LoadMatterTeam" method and use the unique IntApp Walls-managed matter team ID number "matterTeamID" variable obtained from executing the "GetMatterTeamForMatter" method to call the "LoadMatterTeam" method to retrieve the matter team. Iterate through the "UserMemberships" collection within the matter team and output the user team ID and role to the console.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> intapp_web_api.<span class="hljs-function">MatterTeam <span class="hljs-title">LoadMatterTeam</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> matterTeamID</span>)</span>
{
  intapp_web_api.MatterTeam matterTeam = <span class="hljs-keyword">new</span> intapp_web_api.MatterTeam();

  <span class="hljs-keyword">try</span>
  {
    intapp_web_api.APIServiceClient intapp_web_api = <span class="hljs-keyword">new</span> intapp_web_api.APIServiceClient();
    matterTeam = intapp_web_api.LoadMatterTeam(wallscaseteamid);
  }
  <span class="hljs-keyword">catch</span> (Exception ex)
  {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Exception(ex.Message.ToString());
  }

  <span class="hljs-keyword">return</span> matterTeam;
}

MatterTeam the_matter_team_list = LoadMatterTeam(wallscaseteamid);

<span class="hljs-keyword">using</span> (APIServiceClient intapp_web_api = <span class="hljs-keyword">new</span> APIServiceClient())
{
  <span class="hljs-comment">// iterate through the usermemberships collection in the matterteam</span>
  <span class="hljs-keyword">foreach</span> (UserMembership user <span class="hljs-keyword">in</span> the_matter_team_list.UserMemberships)
  {
    <span class="hljs-keyword">string</span> _userid = user.UserId.ToString(); <span class="hljs-comment">// get the user id</span>
    <span class="hljs-keyword">string</span> _therole = user.Role.ToString(); <span class="hljs-comment">// get the user role</span>

    <span class="hljs-comment">// output the user team id and role to the console</span>
    Console.WriteLine(<span class="hljs-string">$"user team id: <span class="hljs-subst">{_userid}</span>"</span>);
    Console.WriteLine(<span class="hljs-string">$"user team role: <span class="hljs-subst">{_therole}</span>"</span>);
  }
}
</code></pre>
<p><strong>Example: Adding a New Member to an Existing Matter Team Membership</strong><br />Building on the "GetMatterTeamForMatter" and "LoadMatterTeam" operations to retrieve a list of matter team members, the following code snippet demonstrates how to use the IntApp Walls API to check existing team membership and add a new member to the team if they are not already a member.</p>
<p>Notes:<br />• Manipulating IntApp Walls teams via the IntApp API requires specific privileges, which are beyond the scope of this document. The requester will also need to be in an IntApp Walls matter admin role as defined in IntApp Walls.<br />• Working with the IntApp API typically requires specific privileges, often granted to a service account with appropriate IntApp Walls access.<br />• References to "intapp_web_api" in the code snippet below, refers to the name of your IntApp API service reference as defined in Visual Studio.</p>
<p><a target="_blank" href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5de27rneo55wbb2x7pef.png"><img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5de27rneo55wbb2x7pef.png" alt="Image description" /></a></p>
<p>Step 1: Using the "GetDMSUserID" operation, Get the "sAMAccountName" of the user you want to add to the Walls team.<br />The "sAMAccountName" (Security Account Manager Account Name) is an attribute in Microsoft Active Directory (AD) that represents a user's logon name used to authenticate to the domain.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">string</span> theid = <span class="hljs-string">"jsmith"</span>; <span class="hljs-comment">// the sAMAccountName ad account name of user to add</span>
<span class="hljs-keyword">string</span> wallsuserid = <span class="hljs-keyword">string</span>.Empty;

wallsuserid = intapp_web_api.GetDMSUserID(UserIDSource.WindowsNetworkLogon, <span class="hljs-string">$@"YourDomainName\<span class="hljs-subst">{theid}</span>"</span>) <span class="hljs-comment">// change "YourDomainName" to your domain name</span>

<span class="hljs-comment">// check if wallsuserid contains a value</span>
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">string</span>.IsNullOrEmpty(wallsuserid))
{
  Console.WriteLine(<span class="hljs-string">"the user you are trying to add to Walls team does not exists in Walls"</span>);
  <span class="hljs-keyword">return</span>;
}
</code></pre>
<p>Step 2: Check if the Matter exists in Walls.</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">string</span> matterID = <span class="hljs-string">"01234"</span>; <span class="hljs-comment">// matterID supplied by you</span>

<span class="hljs-keyword">try</span>
{
  matterTeamID = intapp_web_api.GetMatterTeamForMatter(matterID);
}
<span class="hljs-keyword">catch</span> (Exception ex)
{
  <span class="hljs-keyword">if</span> (ex.Message.Contains(<span class="hljs-string">"The matter"</span>) &amp;&amp; ex.Message.Contains(<span class="hljs-string">"does not exist"</span>))
  {
    Console.WriteLine(<span class="hljs-string">"the matter does do not exist"</span>);
    <span class="hljs-keyword">return</span>;
  }
  <span class="hljs-keyword">else</span>
  {
    Console.WriteLine(ex.Message);
    <span class="hljs-keyword">return</span>;
  }
}
</code></pre>
<p>Step 3: If the Matter exists, is the user already a team member?</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// is the user you are trying to add already on the case team?</span>
<span class="hljs-comment">// use matterTeamID from step #2 above</span>
<span class="hljs-comment">// use wallsuserid from step #2 above</span>
MatterTeam matterteamlist = intapp_web_api.LoadMatterTeamMembership(matterTeamID);
<span class="hljs-keyword">foreach</span>(UserMembership user <span class="hljs-keyword">in</span> matterteamlist.UserMemberships)
{
  <span class="hljs-keyword">if</span> (smaccountname == wallsuserid) <span class="hljs-comment">// are they on the case team already?</span>
  {
    Console.WriteLine(<span class="hljs-string">"the user you are trying to add is already a member"</span>);
    <span class="hljs-keyword">return</span>;
  }
}
</code></pre>
<p>Step 4: Will adding the user to the Matter team cause an internal conflict?</p>
<pre><code class="lang-cpp"><span class="hljs-comment">// will adding the user to the case team cause a conflict?</span>
<span class="hljs-comment">// use wallsuserid from step #2 above</span>

<span class="hljs-built_in">string</span> matterID = <span class="hljs-string">"01234"</span>; <span class="hljs-comment">// matterID supplied by you</span>

<span class="hljs-built_in">string</span>[] warning = intapp_web_api.GetWarningsIfUserIsIncluded(null, wallsuserid, SecurableEntityType.Matter, matterID);
<span class="hljs-keyword">if</span> (warning.Length &gt; <span class="hljs-number">0</span> &amp;&amp; warning[<span class="hljs-number">0</span>].Contains(<span class="hljs-string">"has conflicting case-level access"</span>))
{
  Console.WriteLine(<span class="hljs-string">"adding the user as a member will cause a conflict"</span>);
  <span class="hljs-keyword">return</span>;
}
</code></pre>
<p>Step 5: Finally, add the user to the Matter team.</p>
<pre><code class="lang-csharp"><span class="hljs-comment">// Add the user to the Matter team</span>
<span class="hljs-comment">// use wallsuserid from step #2 above</span>
<span class="hljs-comment">// use matterTeamID from step #3 above</span>
<span class="hljs-comment">// UserMembership is a partial public class</span>
<span class="hljs-comment">// of the Wallbuilder API service reference</span>

<span class="hljs-keyword">string</span> caseteamrole = <span class="hljs-string">"Member"</span>;
<span class="hljs-keyword">string</span> theReason = <span class="hljs-string">$"<span class="hljs-subst">{theid}</span> added programmatically via the Walls API Service"</span>

UserMembership _mbr = <span class="hljs-keyword">new</span>() <span class="hljs-comment">// _mbr membership string array</span>
{
  UserId = wallsuserid,
  Role = caseteamrole,
  Reason = theReason
};

<span class="hljs-comment">// build user membership- must be string array for AddUsersToMatterTeam</span>
UserMembership[] TheAddUserMmbrshipList = { _mbr };

<span class="hljs-comment">// actually add the user now</span>
intapp_web_api.AddUsersToMatterTeam(matterTeamID, UserIDSource.DMS, TheAddUserMmbrshipList); <span class="hljs-comment">// do the add</span>
</code></pre>
<p><strong>Conclusion</strong><br />The IntApp Walls API offers a comprehensive set of operations for managing matter team memberships and safeguarding sensitive information. From retrieving team details to adding new members while checking for conflicts, these API functions enable seamless integration with your workflows and adherence to ethical wall policies. With the right implementation, managing matter teams becomes a streamlined and efficient process that upholds data integrity.</p>
]]></content:encoded></item><item><title><![CDATA[Understanding SQL Transactions: Implicit vs Explicit]]></title><description><![CDATA[In SQL, transactions are essential for ensuring data integrity, consistency, and reliability during database operations. They provide a way to group multiple actions into a single unit of work, ensuring that either all changes are committed or none a...]]></description><link>https://blog.seandrew.info/understanding-sql-transactions-implicit-vs-explicit</link><guid isPermaLink="true">https://blog.seandrew.info/understanding-sql-transactions-implicit-vs-explicit</guid><category><![CDATA[T-SQL]]></category><category><![CDATA[SQL]]></category><category><![CDATA[SQL Server]]></category><category><![CDATA[#SQLtutorial ]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Fri, 27 Dec 2024 17:20:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735319921975/e864b28c-fc13-4b25-bc99-b176d789faeb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In SQL, transactions are essential for ensuring data integrity, consistency, and reliability during database operations. They provide a way to group multiple actions into a single unit of work, ensuring that either all changes are committed or none are applied in case of failure (Atomic).</p>
<p>While transactions can be managed explicitly using specific commands (explicit transactions), SQL Server also handles transactions automatically (implicit transactions) for simpler operations.</p>
<p>Including BEGIN TRANSACTION, COMMIT, and ROLLBACK in your SQL code is a good practice when the operation involves multiple steps or when there is a need to ensure atomicity (all steps succeed or none are applied). If the UPDATE operation is the only operation you are performing and does not depend on other complex logic, you do not need to manually include explicit transactions because SQL Server, by default, wraps single UPDATE statements in an implicit transaction.</p>
<p><strong>Transaction boundaries</strong><br />Transaction boundaries define the start and end points of a database transaction. They encapsulate a series of operations that must be treated as a single, atomic unit. Within these boundaries, either all operations succeed (commit) or none are applied (rollback) to ensure consistency and integrity of the data.</p>
<p>Components of Transaction Boundaries</p>
<ol>
<li><p>Start of a Transaction:</p>
<ul>
<li><p>Marked by BEGIN TRANSACTION in SQL.</p>
</li>
<li><p>Indicates the beginning of a logical group of operations.</p>
</li>
</ul>
</li>
<li><p>Commit (Successful End):</p>
<ul>
<li><p>Marked by COMMIT TRANSACTION.</p>
</li>
<li><p>Saves all the changes made during the transaction to the database</p>
</li>
</ul>
</li>
<li><p>Rollback (Unsuccessful End):</p>
<ul>
<li><p>Marked by ROLLBACK TRANSACTION.</p>
</li>
<li><p>Undoes all changes made during the transaction.</p>
</li>
<li><p>Used when an error occurs or a condition fails within the transaction boundary.</p>
</li>
</ul>
</li>
<li><p>Implicit and Explicit Transactions:</p>
<ul>
<li><p>Implicit Transactions: SQL Server wraps individual DML statements like UPDATE, INSERT, or DELETE in a transaction automatically.</p>
</li>
<li><p>Explicit Transactions: Developers explicitly define the transaction boundaries using BEGIN TRANSACTION, COMMIT, and ROLLBACK.</p>
</li>
</ul>
</li>
</ol>
<p><strong>The Two Transaction Types</strong><br />Implicit</p>
<ul>
<li><p>Implicit transactions are automatically managed by SQL Server and automatically start a transaction whenever a data-modifying statement such as INSERT, UPDATE, or DELETE is executed. This is helpful in situations where you want SQL Server to handle the transaction for you without having to manually manage it. This is ideal for simple scripts where you don't need full control over transaction boundaries.</p>
</li>
<li><p>Implicit transactions are tied to the connection context. If the connection is closed or disconnected, then the transaction will be rolled back automatically.</p>
</li>
</ul>
<p>Explicit</p>
<ul>
<li><p>Explicit transactions are not automatically managed by SQL Server and require you to define (and manage) the transaction boundaries of your SQL actions using the BEGIN TRANSACTION, COMMIT TRANSACTION, and ROLLBACK TRANSACTION commands. This allows more control over when the transaction begins and ends.</p>
</li>
<li><p>Explicit transactions can span multiple operations and even across multiple batches or procedures as long as the same connection remains open.</p>
</li>
</ul>
<p><strong>Atomicity, Consistency, Isolation, Durability (ACID) Properties</strong> <em>(a lot of highfalutin words)</em></p>
<p>Atomicity:<br />Transactions are atomic. What this means is that a transaction (UPDATE, INSERT, or DELETE) will either complete entirely or not at all (basically a ROLLBACK).</p>
<p>Consistency:<br />Transactions move the database from one consistent state to another.<br />Basically, what this means is that transactions ensure that the database remains in a valid state before and after a transaction. It means all defined rules (constraints, relationships, etc.) are adhered to, preventing data corruption or invalid states. If a transaction violates any constraint, the database reverts to its previous consistent state (basically a ROLLBACK).</p>
<p>Isolation:<br />This ensures that concurrent transactions (running at the same time) do not interfere with each other which maintains the integrity of each transactions operations. Basically, it allows transactions to execute as if they were running sequentially, preventing issues like dirty reads, non-repeatable reads, or phantom reads, depending on the isolation level.</p>
<p>Durability:<br />Once a transaction is committed, the changes are permanent, even in the event of a system failure.</p>
<p><strong>When to Include Transactions:</strong></p>
<ul>
<li><p>If multiple statements need to succeed or fail as a group (ACID).</p>
</li>
<li><p>If the operation involves dependencies or cascading changes across multiple tables. An example of something like this would be deleting a primary parent (ex. customer) record, which requires cascading deletions of all related child records (ex. orders and payments) to maintain referential integrity across the tables.</p>
</li>
<li><p>If future enhancements might make the procedure more complex.</p>
</li>
</ul>
<p><strong>Performance Considerations</strong></p>
<ul>
<li><p>Implicit transactions could unintentionally incur additional overhead due to SQL Server managing the transaction boundaries automatically.</p>
</li>
<li><p>Explicit transactions can sometimes be more efficient because you have complete control over when transactions start and end (the boundaries).</p>
</li>
<li><p>In a high-performance environment you should use explicit transactions to minimize unnecessary transaction overhead and ensure that transactions only last as long as necessary.</p>
</li>
</ul>
<p><strong>Some Randomly Related Stuff</strong><br />Error Handling and <strong>Explicit</strong> Transactions<br />Always use error handling (TRY...CATCH), keep your transactions as short as you can, and only include statements that need to be part of the same transaction.</p>
<p>TRY...CATCH blocks are used to catch errors and perform a ROLLBACK if something goes wrong.</p>
<p>Error Handling and <strong>Implicit</strong> Transactions<br />Implicit transactions are more prone to unintended commits if error handling is not properly implemented.</p>
<p>What does this mean? Implicit transactions are automatically committed (by SQL Server) after each individual statement completes successfully (INSERT, UPDATE, DELETE, etc.) without explicit control over the transaction boundaries. If an error occurs mid-operation then the already completed statements cannot be rolled back. This could leave your database in an inconsistent state. Because of this, using error handling and explicit transactions (BEGIN, COMMIT, ROLLBACK) provide you with greater control and ensure atomicity and consistency.</p>
<p><strong>A very simplistic TSQL example of Explicit Transaction</strong><br /><em>This example code snippet is for illustrative purposes only and does not necessarily represent best coding practices.</em></p>
<pre><code class="lang-json">declare @theid int = <span class="hljs-number">5</span>

begin try
  begin transaction MyNamedTransaction

    -- insert into <span class="hljs-string">"YourSQLTable"</span>
    insert into [dbo].[YourSQLTable] (Column1, Column2, Column3, Column4, Column5)
    values (@theid, 'some text value', <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, getdate())

    -- update <span class="hljs-string">"AnotherSQLTable"</span>
    update [dbo].[AnotherSQLTable]
    set [somecolumn] = <span class="hljs-number">1</span>,
    [anothercolumn] = (select [columnname] from [dbo].[YourSQLTable] where [pid] = @theid)
    where [cid] = @theid

  commit transaction MyNamedTransaction
end try
begin catch
  if @@trancount &gt; <span class="hljs-number">0</span>
  begin  
    rollback transaction MyNamedTransaction
  end    
  declare @message nvarchar(<span class="hljs-number">2048</span>) = error_message()
  insert into [dbo].[MyErrorLogTable] (ErrorMessage, ErrorDate) values (@message, getdate())
  throw
end catch
</code></pre>
<p><strong>Conclusion</strong><br />Choosing between implicit and explicit transactions depends on the complexity of your database operations.</p>
<p>Implicit transactions are ideal for simple, single-step actions that require atomicity without the need for explicit transaction management.</p>
<p>As operations become more complex and involve multiple actions or tables, explicit transactions provide more control and flexibility.</p>
<p>By understanding implicit and explicit transactions (and when to use them or not), you can ensure your SQL code is both efficient and reliable, maintaining data integrity and minimizing errors.</p>
]]></content:encoded></item><item><title><![CDATA[Gathering Keypad Input in Voice Calls with Twilio and .NET]]></title><description><![CDATA[Twilio, a cloud communications platform, enables developers to integrate communication features such as SMS, voice, video, and email into applications effortlessly. Among its many capabilities is the ability to gather user keypad input (DTMF tones) d...]]></description><link>https://blog.seandrew.info/gathering-keypad-input-in-voice-calls-with-twilio-and-net-1</link><guid isPermaLink="true">https://blog.seandrew.info/gathering-keypad-input-in-voice-calls-with-twilio-and-net-1</guid><category><![CDATA[C#]]></category><category><![CDATA[twilio]]></category><category><![CDATA[twiliovoice]]></category><category><![CDATA[.NET]]></category><category><![CDATA[API]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Thu, 26 Dec 2024 16:32:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735230621116/c930064b-222d-47be-b427-81e8f11b0984.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Twilio, a cloud communications platform, enables developers to integrate communication features such as SMS, voice, video, and email into applications effortlessly. Among its many capabilities is the ability to gather user keypad input (DTMF tones) during a voice call. This guide walks you through the process of implementing a feature that collects keypad input and responds dynamically in a .NET application.</p>
<p><strong>Overview of Twilio’s Gather Verb</strong></p>
<p>The <strong>Gather</strong> verb in TwiML (Twilio Markup Language) is used to capture user input during a call. By using the Gather verb, you can prompt users to press keys on their phone’s keypad and respond based on their input.</p>
<p><strong>Step 1: Set Up an Endpoint for Keypad Input</strong></p>
<p>To begin, create an endpoint using <a target="_blank" href="http://asp.net/"><strong>ASP.NET</strong></a> Core to serve TwiML that includes the Gather verb. This endpoint provides instructions to the caller and waits for a single digit of input.</p>
<p><strong>Example Implementation</strong></p>
<p><strong>Copy</strong></p>
<p><strong>Copy</strong></p>
<pre><code class="lang-json">using Microsoft.AspNetCore.Mvc;
using Twilio.TwiML;
using Twilio.TwiML.Voice;

[Route(<span class="hljs-string">"api/[controller]"</span>)]
[ApiController]
public class VoiceController : ControllerBase
{
  [HttpGet(<span class="hljs-attr">"gather"</span>)]
  public IActionResult Gather()
  {
    var response = new VoiceResponse();

    <span class="hljs-comment">// create a Gather verb to listen for keypad input</span>
    var gather = new Gather(
      input: new List&lt;Gather.InputEnum&gt; { Gather.InputEnum.Dtmf }, <span class="hljs-comment">// listen for keypad input</span>
      numDigits: <span class="hljs-number">1</span>, <span class="hljs-comment">// gather 1 digit</span>
      action: new Uri(<span class="hljs-string">"https://yourapp.com/api/voice/handle-key"</span>) <span class="hljs-comment">// redirect to handle-key endpoint after gathering input</span>
    );

    gather.Say(<span class="hljs-string">"Press 1 for sales, 2 for support, or 3 for billing."</span>); <span class="hljs-comment">// add instructions to the Gather verb</span>
    response.Append(gather); <span class="hljs-comment">// add the Gather verb to the response</span>
    response.Say(<span class="hljs-string">"We didn’t receive any input. Please try again."</span>); <span class="hljs-comment">// provide a fallback if no input is gathered</span>

    return Content(response.ToString(), <span class="hljs-attr">"application/xml"</span>);
  }
}
</code></pre>
<p><strong>Step 2: Handle Keypad Input</strong></p>
<p>Create another endpoint to handle the user’s input and respond based on the keypad digit they entered. Once the user provides input, this endpoint processes the received digit and returns an appropriate response based on their selection.</p>
<p><strong>Example Implementation</strong></p>
<p><strong>Copy</strong></p>
<p><strong>Copy</strong></p>
<pre><code class="lang-json">[HttpPost(<span class="hljs-string">"handle-key"</span>)]
public IActionResult HandleKey([FromForm] string Digits)
{
  var response = new VoiceResponse();

  <span class="hljs-comment">// check the gathered digit and respond accordingly</span>
  switch (Digits)
  {
    case <span class="hljs-attr">"1"</span>:
      response.Say(<span class="hljs-string">"You pressed 1. Connecting you to sales."</span>);
      <span class="hljs-comment">// Here you could redirect to another call or add more actions</span>
      break;
    case <span class="hljs-string">"2"</span>:
      response.Say(<span class="hljs-string">"You pressed 2. Connecting you to support."</span>);
      <span class="hljs-comment">// Redirect to support, etc.</span>
      break;
    case <span class="hljs-string">"3"</span>:
      response.Say(<span class="hljs-string">"You pressed 3. Connecting you to billing."</span>);
      <span class="hljs-comment">// Redirect to billing, etc.</span>
      break;
    default:
      response.Say(<span class="hljs-string">"Invalid option. Please try again."</span>);
      response.Redirect(new Uri(<span class="hljs-string">"https://yourapp.com/api/voice/gather"</span>)); <span class="hljs-comment">// Redirect back to gather if invalid input</span>
      break;
  }

  return Content(response.ToString(), <span class="hljs-string">"application/xml"</span>);
}
</code></pre>
<p><strong>Step 3: Initiate the Call</strong></p>
<p>Trigger the voice call and direct it to the endpoint created for gathering input (<code>/api/voice/gather</code>).</p>
<p><strong>Example Implementation</strong></p>
<p><strong>Copy</strong></p>
<p><strong>Copy</strong></p>
<pre><code class="lang-json">[HttpPost(<span class="hljs-string">"handle-key"</span>)]
public IActionResult HandleKey([FromForm] string Digits)
{
  var response = new VoiceResponse();

  <span class="hljs-comment">// check the gathered digit and respond accordingly</span>
  switch (Digits)
  {
    case <span class="hljs-attr">"1"</span>:
      response.Say(<span class="hljs-string">"You pressed 1. Connecting you to sales."</span>);
      <span class="hljs-comment">// Here you could redirect to another call or add more actions</span>
      break;
    case <span class="hljs-string">"2"</span>:
      response.Say(<span class="hljs-string">"You pressed 2. Connecting you to support."</span>);
      <span class="hljs-comment">// Redirect to support, etc.</span>
      break;
    case <span class="hljs-string">"3"</span>:
      response.Say(<span class="hljs-string">"You pressed 3. Connecting you to billing."</span>);
      <span class="hljs-comment">// Redirect to billing, etc.</span>
      break;
    default:
      response.Say(<span class="hljs-string">"Invalid option. Please try again."</span>);
      response.Redirect(new Uri(<span class="hljs-string">"https://yourapp.com/api/voice/gather"</span>)); <span class="hljs-comment">// Redirect back to gather if invalid input</span>
      break;
  }

  return Content(response.ToString(), <span class="hljs-string">"application/xml"</span>);
}
</code></pre>
<p><strong>Step 3: Initiate the Call</strong></p>
<p>Trigger the voice call and direct it to the endpoint created for gathering input (<code>/api/voice/gather</code>).</p>
<p><strong>Example Implementation</strong></p>
<p><strong>Copy</strong></p>
<p><strong>Copy</strong></p>
<pre><code class="lang-json">var call = CallResource.Create(
  to: new PhoneNumber(<span class="hljs-string">"+1234567890"</span>), <span class="hljs-comment">// recipient's phone number</span>
  from: new PhoneNumber(<span class="hljs-string">"+0987654321"</span>), <span class="hljs-comment">// your Twilio phone number</span>
  url: new Uri(<span class="hljs-string">"https://yourapp.com/api/voice/gather"</span>)
);

Console.WriteLine($<span class="hljs-string">"Voice call initiated with SID: {call.Sid}"</span>);
</code></pre>
<p><strong>Complete Flow Overview</strong></p>
<p><em>Call Initiation:</em> A voice call is initiated, and the user is directed to the /api/voice/gather endpoint.</p>
<p><em>Gather Input:</em> The user hears a message and provides input by pressing a key on their phone.</p>
<p><em>Input Handling:</em> The digit entered is sent to the /api/voice/handle-key endpoint, which responds based on the user’s choice.</p>
<p><em>Redirection:</em> If the input is invalid, the user is redirected back to the gather step for another attempt.</p>
<p><strong>Conclusion</strong></p>
<p>Using Twilio’s Gather verb in combination with .NET provides a powerful way to collect user input during calls and handle it dynamically. This approach is ideal for creating interactive voice response (IVR) systems, ensuring a seamless user experience for callers. By implementing TwiML endpoints, you can prompt users, process their input, and respond with tailored actions, all while leveraging Twilio’s scalable communications platform.</p>
]]></content:encoded></item><item><title><![CDATA[Consuming and Processing JSON Stream Data in an API Webhook using C#]]></title><description><![CDATA[Recently, I was tasked with developing a custom webhook for our API that would consume a specific client-supplied point-of-sale (POS) JSON stream, parse the data, and write it to a series of custom SQL tables. The provided POS JSON stream includes de...]]></description><link>https://blog.seandrew.info/consuming-and-processing-json-stream-data-in-an-api-webhook-using-c</link><guid isPermaLink="true">https://blog.seandrew.info/consuming-and-processing-json-stream-data-in-an-api-webhook-using-c</guid><category><![CDATA[C#]]></category><category><![CDATA[api]]></category><category><![CDATA[json]]></category><dc:creator><![CDATA[Sean M. Drew Sr.]]></dc:creator><pubDate>Thu, 26 Dec 2024 16:05:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1735229269344/20359638-31b7-4bd8-b97e-059af4926e9a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I was tasked with developing a custom webhook for our API that would consume a specific client-supplied point-of-sale (POS) JSON stream, parse the data, and write it to a series of custom SQL tables. The provided POS JSON stream includes detailed item information, payment type, customer ID, and client-specific transaction identifiers. This process required efficiently handling large, continuous data streams while ensuring that the data was processed accurately and securely stored in the appropriate database tables.</p>
<p>The first step was to break down the high-level tasks and produce a Level of Effort (LOE) estimate. While I won't go into detail on each step here, the primary stages included Requirements Gathering and Planning, Webhook Setup, Stream Deserialization and Validation, Database Design and Mapping, SQL Database Work, Testing and Debugging, and finally, Deployment and Documentation.</p>
<p>Below is a very brief breakdown of the development process based on this sample client JSON.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"OrderID"</span>: <span class="hljs-string">"123"</span>,
  <span class="hljs-attr">"OrderType"</span>: <span class="hljs-string">"Cash"</span>,
  <span class="hljs-attr">"SubTotal"</span>: <span class="hljs-number">8.24</span>,
  <span class="hljs-attr">"StoreID"</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">"CashierID"</span>: <span class="hljs-number">-99</span>,
  <span class="hljs-attr">"CustomerID"</span>: <span class="hljs-number">3481142</span>,
  <span class="hljs-attr">"Time"</span>: <span class="hljs-string">"2020-03-21T10:17:46.3093832-04:00"</span>,
  <span class="hljs-attr">"SalesTax"</span>: <span class="hljs-number">0.0</span>,
  <span class="hljs-attr">"Total"</span>: <span class="hljs-number">8.24</span>,
  <span class="hljs-attr">"TransactionTypeID"</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">"Comment"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"TransactionNumber"</span>: <span class="hljs-number">56355116</span>,
  <span class="hljs-attr">"Status"</span>: <span class="hljs-number">0</span>,
  <span class="hljs-attr">"TransactionEntries"</span>: [
    {
      <span class="hljs-attr">"ID"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"TransactionNumber"</span>: <span class="hljs-number">56355116</span>,
      <span class="hljs-attr">"ItemID"</span>: <span class="hljs-number">96197</span>,
      <span class="hljs-attr">"description"</span>: <span class="hljs-string">"aspirin"</span>,
      <span class="hljs-attr">"Cost"</span>: <span class="hljs-number">0.0</span>,
      <span class="hljs-attr">"FullPrice"</span>: <span class="hljs-number">2.99</span>,
      <span class="hljs-attr">"Price"</span>: <span class="hljs-number">2.99</span>,
      <span class="hljs-attr">"Quantity"</span>: <span class="hljs-number">1.0</span>,
      <span class="hljs-attr">"Taxable"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"SalesTax"</span>: <span class="hljs-number">0.0</span>,
      <span class="hljs-attr">"TaxAmount"</span>: <span class="hljs-number">0.0</span>,
      <span class="hljs-attr">"Comment"</span>: <span class="hljs-string">""</span>,
      <span class="hljs-attr">"DateCreated"</span>: <span class="hljs-string">"2021-05-10T10:17:26.692389-04:00"</span>,
      <span class="hljs-attr">"Notes"</span>: <span class="hljs-string">""</span>,
      <span class="hljs-attr">"ItemLookupCode"</span>: <span class="hljs-string">"002414763521"</span>,
      <span class="hljs-attr">"SellByWeight"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"CategoryName"</span>: <span class="hljs-string">"Misc."</span>,
      <span class="hljs-attr">"OptionDescription"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"OptionDescriptionPrice"</span>: <span class="hljs-literal">null</span>
    },
    {
      <span class="hljs-attr">"ID"</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">"TransactionNumber"</span>: <span class="hljs-number">56355116</span>,
      <span class="hljs-attr">"ItemID"</span>: <span class="hljs-number">96100</span>,
      <span class="hljs-attr">"description"</span>: <span class="hljs-string">"tylenol"</span>,
      <span class="hljs-attr">"Cost"</span>: <span class="hljs-number">0.0</span>,
      <span class="hljs-attr">"FullPrice"</span>: <span class="hljs-number">5.25</span>,
      <span class="hljs-attr">"Price"</span>: <span class="hljs-number">5.25</span>,
      <span class="hljs-attr">"Quantity"</span>: <span class="hljs-number">1.0</span>,
      <span class="hljs-attr">"Taxable"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"SalesTax"</span>: <span class="hljs-number">0.0</span>,
      <span class="hljs-attr">"TaxAmount"</span>: <span class="hljs-number">0.0</span>,
      <span class="hljs-attr">"Comment"</span>: <span class="hljs-string">""</span>,
      <span class="hljs-attr">"DateCreated"</span>: <span class="hljs-string">"2021-05-10T10:17:26.692389-04:00"</span>,
      <span class="hljs-attr">"Notes"</span>: <span class="hljs-string">""</span>,
      <span class="hljs-attr">"ItemLookupCode"</span>: <span class="hljs-string">"123456763521"</span>,
      <span class="hljs-attr">"SellByWeight"</span>: <span class="hljs-literal">false</span>,
      <span class="hljs-attr">"CategoryName"</span>: <span class="hljs-string">"Misc."</span>,
      <span class="hljs-attr">"OptionDescription"</span>: <span class="hljs-literal">null</span>,
      <span class="hljs-attr">"OptionDescriptionPrice"</span>: <span class="hljs-literal">null</span>
    }
  ]
}
</code></pre>
<p><strong>Step 1: Setup a new Webhook and Controller</strong><br />To set up the webhook and its corresponding controller, I configured a route in the app.MapControllerRoute method as follows:</p>
<pre><code class="lang-csharp">app.MapControllerRoute
(
  name: <span class="hljs-string">"POSTransactionExportCL"</span>,
  pattern: <span class="hljs-string">"{controller}/{action}"</span>,
  defaults: <span class="hljs-keyword">new</span> { controller = <span class="hljs-string">"TheClientControllerName"</span>, action = <span class="hljs-string">"POSTransactionExportCL"</span> }
);
</code></pre>
<p>Note: <em>TheClientControllerName</em> is a placeholder for the actual controller name used in the application. I've replaced the actual name in this example for clarity and confidentiality.</p>
<p><strong>Step 2: Define Model Classes for the JSON Stream</strong><br />To parse the JSON stream provided by the client, I created two model classes: POSTransactionExportCL_Order and POSTransactionExportCL_TransactionEntry. These classes define the structure of the JSON data and include all the necessary properties for orders and their associated transaction entries. By using these models, the JSON stream is deserialized into a strongly typed structure, ensuring accurate data processing, and simplifying further operations in the application.</p>
<p>Order Model</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">POSTransactionExportCL_Order</span>
{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> OrderID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? OrderType { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> SubTotal { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> StoreID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> CashierID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> CustomerID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> DateTime Time { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> SalesTax { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> Total { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> TransactionTypeID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Comment { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> TransactionNumber { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Status { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> POSTransactionExportCL_TransactionEntry[]? TransactionEntries { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p>Transaction Entry Model</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">POSTransactionExportCL_TransactionEntry</span>
{
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> ID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> TransactionNumber { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> ItemID { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Description { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> Cost { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> FullPrice { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> Price { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> Quantity { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> Taxable { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> SalesTax { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span> TaxAmount { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Comment { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> DateTime DateCreated { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? Notes { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? ItemLookupCode { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">bool</span> SellByWeight { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? CategoryName { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span>? OptionDescription { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">decimal</span>? OptionDescriptionPrice { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
}
</code></pre>
<p><strong>Step 3: Created a New "POSTransactionExportCL" Webhook to Handle Incoming Transaction Data from the Client JSON Stream</strong><br />The POSTransactionExportCL webhook in the client controller is designed to securely and efficiently handle incoming transaction data provided in a JSON stream via POST HTTP requests. Acting as a gateway for processing and storing point-of-sale (POS) transaction details, this webhook ensures that data integrity and security remain top priorities throughout its workflow.</p>
<p>This endpoint validates the client's access token (X-Auth-Token), verifies payload integrity, and stores transaction data in the backend database. The processing pipeline includes token validation, payload deserialization, and database interaction via SQL methods. With comprehensive error handling and logging mechanisms, the webhook provides clear feedback to the client in the event of issues, such as invalid tokens, unauthorized access, or payload formatting errors.</p>
<p>The following code snippet provides the complete implementation of the POSTransactionExportCL webhook:</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">HttpPost</span>]
[<span class="hljs-meta">Route(<span class="hljs-meta-string">"{controller}/{action}"</span>)</span>]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">async</span> Task&lt;IActionResult&gt; <span class="hljs-title">POSTransactionExportCL</span>(<span class="hljs-params"></span>)</span>
{
  <span class="hljs-keyword">string</span>? client_token = Request.Headers[<span class="hljs-string">"X-Auth-Token"</span>]; <span class="hljs-comment">// client passed in header - this value is generated from GetPatientData</span>
  <span class="hljs-keyword">string</span>? BodyContent = <span class="hljs-keyword">string</span>.Empty;
  <span class="hljs-keyword">string</span>? actionName = Request.Path; <span class="hljs-comment">// get the IActionResult controller and name (/Base/IActionResult)</span>

  <span class="hljs-keyword">var</span> config = <span class="hljs-keyword">new</span> ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile(<span class="hljs-string">"appsettings.json"</span>); <span class="hljs-comment">// read appsettings.json for config settings</span>
  <span class="hljs-keyword">var</span> configuration = config.Build();

  <span class="hljs-comment">// do the regular token processing</span>
  <span class="hljs-keyword">if</span> (!GeneralFunctions.IsCPTokenValid(client_token!)) <span class="hljs-comment">// JWT token from the header</span>
  {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">"Unauthorized - X-Auth-Token is invalid"</span>, StatusCode = <span class="hljs-number">401</span> };
  }

  <span class="hljs-comment">// make sure the client has access to the webhook</span>
  <span class="hljs-keyword">if</span> (!GeneralFunctions.HasEndpointAccess(actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>])) <span class="hljs-comment">// get everything after the second / and check HasEndpointAccess</span>
  {
    GeneralFunctions.LogEndpointCall(actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>], <span class="hljs-string">$"<span class="hljs-subst">{client_token}</span> Endpoint access denied: <span class="hljs-subst">{actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>]}</span>"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">$"<span class="hljs-subst">{client_token}</span> Endpoint access denied"</span>, StatusCode = <span class="hljs-number">401</span> };
  }


  JObject obj; <span class="hljs-comment">// instantiate empty JObject for reading and processing Body payload</span>
  StreamReader? reader = <span class="hljs-literal">null</span>; <span class="hljs-comment">// instantiate empty nullable StreamReader variable</span>
  <span class="hljs-keyword">try</span>
  {
    <span class="hljs-keyword">var</span> httpResponseMessage = HttpContext.Request.Body;
    <span class="hljs-keyword">using</span>(reader = <span class="hljs-keyword">new</span> StreamReader(httpResponseMessage))
      {
        BodyContent = <span class="hljs-keyword">await</span> reader.ReadToEndAsync(); <span class="hljs-comment">// read the response body</span>
    }

    obj = JObject.Parse(BodyContent); <span class="hljs-comment">// just to see if we get an error processing the body</span>
    <span class="hljs-keyword">if</span> (!obj.HasValues)
    {
      GeneralFunctions.LogEndpointCall(actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>], <span class="hljs-string">$"Error: the payload appreasrs to not contain any values"</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">$"Error: the payload appreasrs to not contain any values"</span>, StatusCode = <span class="hljs-number">500</span> };
    }

    <span class="hljs-comment">// check payload parameters</span>
    <span class="hljs-keyword">string</span> payloaderror = String.Empty;
    <span class="hljs-keyword">try</span>
    {
      <span class="hljs-keyword">if</span> ((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderID"</span>) == <span class="hljs-string">""</span> || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderID"</span>) == String.Empty || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderID"</span>) == <span class="hljs-literal">null</span> || <span class="hljs-keyword">string</span>.IsNullOrEmpty((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderID"</span>))) { payloaderror = <span class="hljs-string">"OrderID"</span>; }
      <span class="hljs-keyword">if</span> ((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderType"</span>) == <span class="hljs-string">""</span> || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderType"</span>) == String.Empty || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderType"</span>) == <span class="hljs-literal">null</span> || <span class="hljs-keyword">string</span>.IsNullOrEmpty((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"OrderType"</span>))) { payloaderror = <span class="hljs-string">"OrderType"</span>; }
      <span class="hljs-keyword">if</span> ((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"CustomerID"</span>) == <span class="hljs-string">""</span> || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"CustomerID"</span>) == String.Empty || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"CustomerID"</span>) == <span class="hljs-literal">null</span> || <span class="hljs-keyword">string</span>.IsNullOrEmpty((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"CustomerID"</span>))) { payloaderror = <span class="hljs-string">"CustomerID"</span>; }
      <span class="hljs-keyword">if</span> ((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"TransactionNumber"</span>) == <span class="hljs-string">""</span> || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"TransactionNumber"</span>) == String.Empty || (<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"TransactionNumber"</span>) == <span class="hljs-literal">null</span> || <span class="hljs-keyword">string</span>.IsNullOrEmpty((<span class="hljs-keyword">string</span>?)obj.SelectToken(<span class="hljs-string">"TransactionNumber"</span>))) { payloaderror = <span class="hljs-string">"TransactionNumber"</span>; }
        <span class="hljs-keyword">if</span> (payloaderror != String.Empty)
        {
        GeneralFunctions.LogEndpointCall(actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>], <span class="hljs-string">$"<span class="hljs-subst">{payloaderror}</span> - <span class="hljs-subst">{payloaderror}</span> missing from payload <span class="hljs-subst">{JObject.Parse(BodyContent)}</span>"</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">$"<span class="hljs-subst">{payloaderror}</span> - <span class="hljs-subst">{payloaderror}</span> missing from payload"</span>, StatusCode = <span class="hljs-number">422</span> };
        }
      }
      <span class="hljs-keyword">catch</span> (Exception)
      {
      GeneralFunctions.LogEndpointCall(actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>], <span class="hljs-string">$"error processing payload: <span class="hljs-subst">{JObject.Parse(BodyContent)}</span>"</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">"error processing payload"</span>, StatusCode = <span class="hljs-number">422</span> };
    }


    POSTransactionExportCL_Order? orderinfo; <span class="hljs-comment">// declares a variable named orderinfo that will hold an instance of both POSTransactionExportCL_Order/POSTransactionExportCL_TransactionEntry classes</span>
    <span class="hljs-keyword">try</span>
    {
      <span class="hljs-comment">// deserialize the JSON stream (body) into the orderinfo object</span>
      orderinfo = JsonConvert.DeserializeObject&lt;POSTransactionExportCL_Order&gt;(BodyContent);
    }
    <span class="hljs-keyword">catch</span> (JsonException ex)
    {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">$"Invalid JSON format: <span class="hljs-subst">{ex.Message}</span>"</span>, StatusCode = <span class="hljs-number">422</span> };
    }

    <span class="hljs-keyword">try</span>
    {
      <span class="hljs-comment">// call the POSTransactionExportCLSQL SQL processing method</span>
      <span class="hljs-comment">// write everything to the POSTransactionExportCL table</span>
      <span class="hljs-comment">// write everything to the POSTransactionExportCLTransactionEntries table</span>
      <span class="hljs-comment">// using cp_APIPOSTransactionExportCL and cp_APIPOSTransactionEntryExportCL</span>
      <span class="hljs-keyword">string</span> status = <span class="hljs-keyword">await</span> POSTransactionExportCLSQL(orderinfo!);
    }
    <span class="hljs-keyword">catch</span> (Exception ex)
    {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">$"Error: POSTransactionExportCLSQL: <span class="hljs-subst">{ex.Message}</span>"</span>, StatusCode = <span class="hljs-number">422</span> };
      }
    }
    <span class="hljs-keyword">catch</span> (Exception)
  {
    GeneralFunctions.LogEndpointCall(actionName.Split(<span class="hljs-string">'/'</span>)[<span class="hljs-number">2</span>], <span class="hljs-string">$"Error reading payload: <span class="hljs-subst">{JObject.Parse(BodyContent)}</span>"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">"Error reading payload"</span>, StatusCode = <span class="hljs-number">500</span> };
  }

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> ContentResult() { Content = <span class="hljs-string">$"POSTransactionExportCL - success - <span class="hljs-subst">{client_token}</span>"</span>, StatusCode = <span class="hljs-number">200</span> };
}
</code></pre>
<p>At its core, the webhook performs essential operations such as validating access tokens, verify that the client has access to the webhook, verify payload integrity, and write transaction data to specific database tables. With a focus on security, it checks the client's authentication and authorization before processing any request. The incoming payload undergoes validation to ensure the required data is present and formatted correctly. Any anomalies in the process are logged for traceability, and detailed error responses are provided to the client for troubleshooting.</p>
<p>Below is an overview of its key components:</p>
<p><strong>Attributes and Routing</strong></p>
<ul>
<li><p>[HttpPost]: The method handles POST requests.</p>
</li>
<li><p>[Route("{controller}/{action}")]: Defines a dynamic route based on the controller and action name.</p>
</li>
</ul>
<p><strong>Primary Steps</strong></p>
<ol>
<li>Token Validation</li>
</ol>
<ul>
<li><p>The client sends an X-Auth-Token in the request headers.</p>
</li>
<li><p>The token is validated using GeneralFunctions.IsCPTokenValid.</p>
</li>
<li><p>If the token is invalid, the method returns a 401 Unauthorized response</p>
</li>
</ul>
<ol start="2">
<li>Access Control</li>
</ol>
<ul>
<li><p>Extracts the endpoint name from the request path.</p>
</li>
<li><p>Uses GeneralFunctions.HasEndpointAccess to determine if the token has access to the requested endpoint.</p>
</li>
<li><p>Logs unauthorized attempts and returns a 401 response if access is denied.</p>
</li>
</ul>
<ol start="3">
<li>Payload Parsing</li>
</ol>
<ul>
<li><p>Reads the request body and ensures it's valid JSON using JObject.Parse.</p>
</li>
<li><p>Checks for required fields (OrderID, OrderType, CustomerID, TransactionNumber).</p>
</li>
<li><p>Logs and returns an error if required fields are missing.</p>
</li>
</ul>
<ol start="4">
<li>Deserialization</li>
</ol>
<ul>
<li><p>Converts the JSON payload into a POSTransactionExportCL_Order object using JsonConvert.DeserializeObject.</p>
</li>
<li><p>Handles any deserialization errors gracefully with appropriate logging.</p>
</li>
</ul>
<ol start="5">
<li>Database Interaction</li>
</ol>
<ul>
<li><p>Processes the order and its transactions using POSTransactionExportCLSQL.</p>
</li>
<li><p>Writes data into tables (POSTransactionExportCL and POSTransactionExportCLTransactionEntries) via stored procedures.</p>
</li>
</ul>
<ol start="6">
<li>Error Handling</li>
</ol>
<ul>
<li><p>Comprehensive error handling is implemented at every stage, including token validation, payload parsing, and database interaction.</p>
</li>
<li><p>Logs specific error details for debugging and returns appropriate HTTP status codes to the client.</p>
</li>
</ul>
<ol start="7">
<li>Response Codes</li>
</ol>
<ul>
<li><p>200 OK: Indicates successful processing.</p>
</li>
<li><p>401 Unauthorized: Token validation or access control failure.</p>
</li>
<li><p>422 Unprocessable Entity: Errors in payload structure or content.</p>
</li>
<li><p>500 Internal Server Error: Unexpected errors during payload reading or processing.</p>
</li>
</ul>
<p><strong>Key Highlights</strong></p>
<ul>
<li><p>Dynamic Configuration: Reads configuration from appsettings.json for flexibility.</p>
</li>
<li><p>Logging: Logs all significant events and errors, aiding in troubleshooting and monitoring.</p>
</li>
<li><p>Validation-Driven Workflow: Ensures token authenticity, endpoint permissions, and payload integrity before proceeding.</p>
</li>
<li><p>Modular Structure: Separates concerns like token validation, payload parsing, and database interaction for better maintainability.</p>
</li>
</ul>
<p><strong>Step 4: The SQL Method</strong><br />The POSTransactionExportCLSQL method is responsible for exporting point-of-sale (POS) order data and its associated transaction entries to the SQL database.</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">async</span> Task&lt;<span class="hljs-keyword">string</span>&gt; <span class="hljs-title">POSTransactionExportCLSQL</span>(<span class="hljs-params">POSTransactionExportCL_Order orderinfo</span>)</span>
{
  <span class="hljs-keyword">int</span> rowsAffected = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">string</span> retval = <span class="hljs-keyword">string</span>.Empty;
  <span class="hljs-keyword">string</span> pkid = <span class="hljs-keyword">string</span>.Empty;

  <span class="hljs-keyword">if</span> (orderinfo == <span class="hljs-literal">null</span>)
  {
    <span class="hljs-keyword">return</span> <span class="hljs-string">"Invalid order information provided."</span>;
  }


  <span class="hljs-keyword">try</span>
  {
    <span class="hljs-keyword">var</span> config = <span class="hljs-keyword">new</span> ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile(<span class="hljs-string">"appsettings.json"</span>); <span class="hljs-comment">// read appsettings.json for config settings</span>
    <span class="hljs-keyword">var</span> configuration = config.Build();
    <span class="hljs-keyword">string</span>? _sql_con_str = configuration[<span class="hljs-string">"sqlsettings:cphdbintegrated"</span>];

    <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> _sql_con = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlConnection(_sql_con_str))
    {
      <span class="hljs-keyword">await</span> _sql_con.OpenAsync();

      <span class="hljs-comment">// insert order data (from orderinfo class)</span>
      <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> _sql_cmd = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlCommand(<span class="hljs-string">"cp_APIPOSTransactionExportCL"</span>, _sql_con))
      {
        _sql_cmd.CommandType = System.Data.CommandType.StoredProcedure;

        <span class="hljs-comment">// Add Order parameters</span>
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@OrderID"</span>, orderinfo.OrderID);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@OrderType"</span>, orderinfo.OrderType);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@SubTotal"</span>, orderinfo.SubTotal);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@StoreID"</span>, orderinfo.StoreID);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@CashierID"</span>, orderinfo.CashierID);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@CustomerID"</span>, orderinfo.CustomerID);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Time"</span>, orderinfo.Time);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@SalesTax"</span>, orderinfo.SalesTax);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Total"</span>, orderinfo.Total);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@TransactionTypeID"</span>, orderinfo.TransactionTypeID);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Comment"</span>, orderinfo.Comment);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@TransactionNumber"</span>, orderinfo.TransactionNumber);
        _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Status"</span>, orderinfo.Status);

        <span class="hljs-comment">// Execute the query and read the result</span>
        <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> reader = <span class="hljs-keyword">await</span> _sql_cmd.ExecuteReaderAsync())
        {
          <span class="hljs-keyword">if</span> (<span class="hljs-keyword">await</span> reader.ReadAsync())
          {
            pkid = reader[<span class="hljs-string">"theGUID"</span>].ToString()!;
          }
        }
      }

      <span class="hljs-keyword">if</span> (IsNull(pkid)) { <span class="hljs-keyword">return</span> <span class="hljs-string">"error - parent pkid is null"</span>; }

      <span class="hljs-keyword">using</span> (<span class="hljs-keyword">var</span> _sql_cmd = <span class="hljs-keyword">new</span> System.Data.SqlClient.SqlCommand(<span class="hljs-string">"cp_APIPOSTransactionEntryExportCL"</span>, _sql_con))
      {
        _sql_cmd.CommandType = System.Data.CommandType.StoredProcedure;

        <span class="hljs-comment">// insert transactionentries data (from POSTransactionExportCL_TransactionEntry)</span>
        <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> entry <span class="hljs-keyword">in</span> orderinfo.TransactionEntries!)
        {
          _sql_cmd.Parameters.Clear();

          <span class="hljs-comment">// Add TransactionEntry parameters</span>
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@pkid"</span>, pkid);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@ID"</span>, entry.ID);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@OrderID"</span>, orderinfo.OrderID);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@TransactionNumber"</span>, entry.TransactionNumber);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@ItemID"</span>, entry.ItemID);

          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Description"</span>, entry.Description ?? <span class="hljs-string">""</span>);

          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Cost"</span>, entry.Cost);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@FullPrice"</span>, entry.FullPrice);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Price"</span>, entry.Price);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Quantity"</span>, entry.Quantity);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Taxable"</span>, entry.Taxable);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@SalesTax"</span>, entry.SalesTax);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@TaxAmount"</span>, entry.TaxAmount);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Comment"</span>, entry.Comment ?? <span class="hljs-string">""</span>);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@DateCreated"</span>, entry.DateCreated);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@Notes"</span>, entry.Notes ?? <span class="hljs-string">""</span>);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@ItemLookupCode"</span>, entry.ItemLookupCode ?? <span class="hljs-string">""</span>);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@SellByWeight"</span>, entry.SellByWeight);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@CategoryName"</span>, entry.CategoryName ?? <span class="hljs-string">""</span>);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@OptionDescription"</span>, entry.OptionDescription ?? <span class="hljs-string">""</span>);
          _sql_cmd.Parameters.AddWithValue(<span class="hljs-string">"@OptionDescriptionPrice"</span>, entry.OptionDescriptionPrice ?? <span class="hljs-number">0</span>);

          <span class="hljs-keyword">await</span> _sql_cmd.ExecuteNonQueryAsync();
        }
      }


    }
  }
  <span class="hljs-keyword">catch</span> (Exception ex)
  {
    <span class="hljs-comment">// log the error to the error logging table</span>
    GeneralFunctions.LogError(<span class="hljs-string">"POSTransactionExportCLSQL"</span>, ex, orderinfo);
    <span class="hljs-keyword">return</span> <span class="hljs-string">$"Error: <span class="hljs-subst">{ex.Message}</span>"</span>;
  }

  <span class="hljs-keyword">return</span> rowsAffected.ToString();
}
</code></pre>
<p>Here is an overview of its key operations:</p>
<p>Input Validation:</p>
<ul>
<li>Validates the orderinfo parameter to ensure it contains valid order data. If null, it returns an error message immediately.</li>
</ul>
<p>Database Connection Setup:</p>
<ul>
<li><p>Reads connection settings from an appsettings.json file using ConfigurationBuilder.</p>
</li>
<li><p>Establishes a SQL connection using the provided connection string.</p>
</li>
</ul>
<p>Order Data Export:</p>
<ul>
<li><p>Executes the stored procedure cp_APIPOSTransactionExportCL to insert order-related data into the database. Parameters such as OrderID, OrderType, SubTotal, and others are mapped from the orderinfo object.</p>
</li>
<li><p>Captures a primary key (theGUID) returned by the stored procedure for use in subsequent operations.</p>
</li>
</ul>
<p>Transaction Entries Export:</p>
<ul>
<li><p>Iterates through the TransactionEntries collection within the orderinfo object.</p>
</li>
<li><p>For each transaction entry, executes the cp_APIPOSTransactionEntryExportCL stored procedure to store detailed transaction data, including item details, pricing, tax information, and additional metadata.</p>
</li>
</ul>
<p>Error Handling:</p>
<ul>
<li><p>Wraps operations in a try-catch block to handle exceptions.</p>
</li>
<li><p>Logs any errors encountered using the GeneralFunctions.LogError method and returns a descriptive error message.</p>
</li>
</ul>
<p>Return Value:</p>
<ul>
<li>Returns the number of rows affected or an appropriate error message.</li>
</ul>
<p>Use Cases<br />This method is well-suited for scenarios requiring:</p>
<ul>
<li><p>Batch processing of POS orders and their line items into a database.</p>
</li>
<li><p>Structured logging of errors for troubleshooting.</p>
</li>
<li><p>Integration with a broader POS or order management system.</p>
</li>
</ul>
<p>Considerations:</p>
<ul>
<li><p>The method relies on two stored procedures for database operations, promoting modularity.</p>
</li>
<li><p>SQL injection risks are mitigated by using parameterized queries.</p>
</li>
</ul>
<p><strong>Step 5: The SQL Table Structure</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">drop</span> <span class="hljs-keyword">table</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> [POSTransactionExportCLTransactionEntries]
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">drop</span> <span class="hljs-keyword">table</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> [POSTransactionExportCL]
<span class="hljs-keyword">go</span>


<span class="hljs-comment">-- parent table (Order Entries)</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> [POSTransactionExportCL]
(
  [pkid] uniqueidentifier <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span> <span class="hljs-keyword">unique</span>,
  [OrderID] <span class="hljs-built_in">int</span>,
  [OrderType] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>),
  [SubTotal] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  [StoreID] <span class="hljs-built_in">int</span>,
  [CashierID] <span class="hljs-built_in">int</span>,
  [CustomerID] <span class="hljs-built_in">int</span>,
  [<span class="hljs-built_in">Time</span>] datetime,
  [SalesTax] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  [Total] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  [TransactionTypeID] <span class="hljs-built_in">int</span>,
  [<span class="hljs-keyword">Comment</span>] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>),
  [TransactionNumber] <span class="hljs-built_in">bigint</span>,
  [<span class="hljs-keyword">Status</span>] <span class="hljs-built_in">int</span>,
  primary <span class="hljs-keyword">key</span> ([pkid]) <span class="hljs-comment">-- define primary key (parent)</span>
  )
<span class="hljs-keyword">go</span>



<span class="hljs-comment">-- child table (Transaction Entries)</span>
<span class="hljs-keyword">create</span> <span class="hljs-keyword">table</span> [POSTransactionExportCLTransactionEntries]
(
  [pkid] uniqueidentifier <span class="hljs-keyword">not</span> <span class="hljs-literal">null</span>, <span class="hljs-comment">-- this will be the pkid value from POSTransactionExportCL</span>
  [<span class="hljs-keyword">ID</span>] <span class="hljs-built_in">int</span>,
  [OrderID] <span class="hljs-built_in">int</span>,
  [TransactionNumber] <span class="hljs-built_in">bigint</span>,
  [ItemID] <span class="hljs-built_in">int</span>,
  [Description] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>) <span class="hljs-literal">null</span>,
  [<span class="hljs-keyword">Cost</span>] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  [FullPrice] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  [Price] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  [Quantity] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  [Taxable] <span class="hljs-built_in">bit</span> <span class="hljs-literal">null</span>,
  [SalesTax] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  [TaxAmount] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  [<span class="hljs-keyword">Comment</span>] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>) <span class="hljs-literal">null</span>,
  [DateCreated] datetime <span class="hljs-literal">null</span>,
  [Notes] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>) <span class="hljs-literal">null</span>,
  [SellByWeight] <span class="hljs-built_in">bit</span> <span class="hljs-literal">null</span>,
  [CategoryName] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>) <span class="hljs-literal">null</span>,
  [OptionDescription] <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>) <span class="hljs-literal">null</span>,
  [OptionDescriptionPrice] <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>) <span class="hljs-literal">null</span>,
  <span class="hljs-keyword">foreign</span> <span class="hljs-keyword">key</span> ([pkid]) <span class="hljs-keyword">references</span> [POSTransactionExportCL] ([pkid]) <span class="hljs-comment">-- define foreign key relationship (Child)</span>
)

<span class="hljs-keyword">go</span>
</code></pre>
<p>The table structure defines a parent-child relationship between POSTransactionExportCL and POSTransactionExportCLTransactionEntries, designed for managing point-of-sale (POS) transactions and their associated entries.</p>
<p>Parent Table: POSTransactionExportCL<br />This table serves as the central repository for order-level transaction data. Each row uniquely identifies an order using the pkid column, a uniqueidentifier serving as the primary key. Key fields include OrderID, OrderType, StoreID, CashierID, and financial details such as SubTotal, SalesTax, and Total. It provides a high-level overview of transactions.</p>
<p>Child Table: POSTransactionExportCLTransactionEntries<br />This table stores detailed line-item entries associated with transactions in the parent table. The pkid column in this table is a foreign key referencing the pkid in the parent table, establishing the relationship. Each row represents an individual item or transaction entry, including details such as ItemID, Description, Price, Quantity, and Taxable status. It supports a comprehensive breakdown of items sold, returned, or discounted.</p>
<p>Key Features:</p>
<ul>
<li><p>The parent table enforces referential integrity through a primary key on pkid.</p>
</li>
<li><p>The child table maintains a foreign key constraint on pkid, ensuring that all entries correspond to a valid transaction in the parent table.</p>
</li>
<li><p>This structure supports efficient querying and reporting, enabling quick joins between orders and their detailed line items.</p>
</li>
</ul>
<p><strong>Step 6: The SQL Stored Procedures</strong><br />cp_APIPOSTransactionExportCL<br />This stored procedure is responsible for populating the POSTransactionExportCL table with detailed transaction data from a Point of Sale (POS) system. It takes multiple parameters representing key attributes of a transaction, such as order information, store and cashier IDs, customer details, and transaction metadata.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">drop</span> <span class="hljs-keyword">procedure</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> [dbo].[cp_APIPOSTransactionExportCL]
<span class="hljs-keyword">go</span>


<span class="hljs-keyword">set</span> ansi_nulls <span class="hljs-keyword">on</span>
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">set</span> quoted_identifier <span class="hljs-keyword">on</span>
<span class="hljs-keyword">go</span>



<span class="hljs-keyword">create</span> <span class="hljs-keyword">procedure</span> [dbo].[cp_APIPOSTransactionExportCL]
  @OrderID <span class="hljs-built_in">int</span>,
  @OrderType <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>),
  @SubTotal <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @StoreID <span class="hljs-built_in">int</span>,
  @CashierID <span class="hljs-built_in">int</span>,
  @CustomerID <span class="hljs-built_in">int</span>,
  @<span class="hljs-built_in">Time</span> datetime,
  @SalesTax <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @Total <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @TransactionTypeID <span class="hljs-built_in">int</span>,
  @<span class="hljs-keyword">Comment</span> <span class="hljs-keyword">nvarchar</span>(<span class="hljs-keyword">max</span>),
  @TransactionNumber <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>),
  @<span class="hljs-keyword">Status</span> <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>)
<span class="hljs-keyword">as</span>
<span class="hljs-keyword">begin</span>
    <span class="hljs-keyword">set</span> nocount <span class="hljs-keyword">on</span>;
    <span class="hljs-keyword">begin</span> try
      <span class="hljs-keyword">declare</span> @theGUID <span class="hljs-keyword">as</span> uniqueidentifier = newid()
      <span class="hljs-keyword">declare</span> @p_namedtransaction <span class="hljs-built_in">varchar</span>(<span class="hljs-number">60</span>) = <span class="hljs-string">'cp_APIPOSTransactionExportCL'</span>

      <span class="hljs-keyword">begin</span> <span class="hljs-keyword">transaction</span> @p_namedtransaction
        <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> POSTransactionExportCL
        (
          [pkid],
          [OrderID],
          [OrderType],
          [SubTotal],
          [StoreID],
          [CashierID],
          [CustomerID],
          [<span class="hljs-built_in">Time</span>],
          [SalesTax],
          [Total],
          [TransactionTypeID],
          [<span class="hljs-keyword">Comment</span>],
          [TransactionNumber],
          [<span class="hljs-keyword">Status</span>]
        )
        <span class="hljs-keyword">values</span>
        (
          @theGUID,
          @OrderID,
          @OrderType,
          @SubTotal,
          @StoreID,
          @CashierID,
          @CustomerID,
          @<span class="hljs-built_in">Time</span>,
          @SalesTax,
          @Total,
          @TransactionTypeID,
          @<span class="hljs-keyword">Comment</span>,
          @TransactionNumber,
          @<span class="hljs-keyword">Status</span>
        )
      <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span> @p_namedtransaction
      <span class="hljs-keyword">select</span> @theGUID <span class="hljs-keyword">as</span> [theGUID] <span class="hljs-comment">-- return for cp_APIPOSTransactionEntryExportCL</span>
    <span class="hljs-keyword">end</span> try
    <span class="hljs-keyword">begin</span> catch
      <span class="hljs-keyword">if</span> @@trancount &gt; <span class="hljs-number">0</span>
        <span class="hljs-keyword">rollback</span> <span class="hljs-keyword">transaction</span> @p_namedtransaction;
      throw;
    <span class="hljs-keyword">end</span> catch
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">go</span>
</code></pre>
<p>Key Features:</p>
<ol>
<li>Primary Purpose</li>
</ol>
<ul>
<li><p>Inserts a new record into the POSTransactionExportCL table using the provided parameters.</p>
</li>
<li><p>Assigns a unique identifier (uniqueidentifier) to each transaction for tracking and reference.</p>
</li>
</ul>
<ol start="2">
<li>Transactional Safety</li>
</ol>
<ul>
<li><p>Uses an explicit named transaction (@p_namedtransaction) to ensure atomicity. This prevents partial inserts by rolling back the transaction in case of an error.</p>
</li>
<li><p>Includes structured error handling with a TRY...CATCH block to handle potential issues during execution.</p>
</li>
</ul>
<ol start="3">
<li>Parameters</li>
</ol>
<ul>
<li><p>The procedure accepts a range of input parameters, such as:</p>
</li>
<li><p>Order Details: @OrderID, @OrderType, @SubTotal, @SalesTax, <a target="_blank" href="https://dev.to/total">@total</a>.</p>
</li>
<li><p>Entity Identifiers: @StoreID, @CashierID, @CustomerID.</p>
</li>
<li><p>Transaction Metadata: <a target="_blank" href="https://dev.to/time">@time</a>, @TransactionTypeID, @Comment, @TransactionNumber, @Status.</p>
</li>
</ul>
<ol start="4">
<li>Output</li>
</ol>
<ul>
<li>Returns the generated unique identifier (@theGUID) for the newly inserted record. This @theGUID variable is then used by cp_APIPOSTransactionEntryExportCL, to insert records into the POSTransactionExportCLTransactionEntries child table.</li>
</ul>
<ol start="5">
<li>Resiliency</li>
</ol>
<ul>
<li><p>The NOCOUNT setting is enabled to reduce overhead from row count messages.</p>
</li>
<li><p>Proper use of BEGIN TRANSACTION and COMMIT TRANSACTION ensures data integrity.</p>
</li>
</ul>
<ol start="6">
<li>Benefits</li>
</ol>
<ul>
<li><p>Data Integrity: The use of transactions ensures that either all or none of the data is written.</p>
</li>
<li><p>Scalability: Designed to handle a variety of transaction scenarios, from in-store to online orders.</p>
</li>
<li><p>Traceability: The generated unique identifier (@theGUID) allows easy identification and troubleshooting.</p>
</li>
</ul>
<p>This procedure plays a critical role in maintaining a reliable record of POS transactions, which is essential for reporting, auditing, and analytics.</p>
<p>POSTransactionExportCLTransactionEntries<br />This stored procedure is designed to populate the POSTransactionExportCLTransactionEntries table with detailed order transaction entries, breaking down the specifics of each item within a transaction and complements the cp_APIPOSTransactionExportCL procedure, which logs the high-level transaction details.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">drop</span> <span class="hljs-keyword">procedure</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> [dbo].[cp_APIPOSTransactionEntryExportCL]
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">set</span> ansi_nulls <span class="hljs-keyword">on</span>
<span class="hljs-keyword">go</span>

<span class="hljs-keyword">set</span> quoted_identifier <span class="hljs-keyword">on</span>
<span class="hljs-keyword">go</span>


<span class="hljs-keyword">create</span> <span class="hljs-keyword">procedure</span> [dbo].[cp_APIPOSTransactionEntryExportCL]
  @pkid uniqueidentifier,
  @<span class="hljs-keyword">ID</span> <span class="hljs-built_in">int</span>,
  @OrderID <span class="hljs-built_in">int</span>,
  @TransactionNumber <span class="hljs-built_in">bigint</span>,
  @ItemID <span class="hljs-built_in">int</span>,
  @Description <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>),
  @<span class="hljs-keyword">Cost</span> <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @FullPrice <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @Price <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @Quantity <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @Taxable <span class="hljs-built_in">bit</span>,
  @SalesTax <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @TaxAmount <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>),
  @<span class="hljs-keyword">Comment</span> <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>),
  @DateCreated datetime,
  @Notes <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>),
  @ItemLookupCode <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>),
  @SellByWeight <span class="hljs-built_in">bit</span>,
  @CategoryName <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">50</span>),
  @OptionDescription <span class="hljs-keyword">nvarchar</span>(<span class="hljs-number">255</span>),
  @OptionDescriptionPrice <span class="hljs-built_in">decimal</span>(<span class="hljs-number">18</span>, <span class="hljs-number">2</span>)
<span class="hljs-keyword">as</span>
<span class="hljs-keyword">begin</span>
  <span class="hljs-keyword">set</span> nocount <span class="hljs-keyword">on</span>;
    <span class="hljs-keyword">begin</span> try
      <span class="hljs-keyword">declare</span> @theGUID <span class="hljs-keyword">as</span> uniqueidentifier = newid()
      <span class="hljs-keyword">declare</span> @p_namedtransaction <span class="hljs-built_in">varchar</span>(<span class="hljs-number">60</span>) = <span class="hljs-string">'cp_APIPOSTransactionExportCL'</span>

      <span class="hljs-keyword">begin</span> <span class="hljs-keyword">transaction</span> @p_namedtransaction
        <span class="hljs-keyword">insert</span> <span class="hljs-keyword">into</span> [POSTransactionExportCLTransactionEntries]
        (
          [pkid],
          [<span class="hljs-keyword">ID</span>],
          [OrderID],
          [TransactionNumber],
          [ItemID],
          [Description],
          [<span class="hljs-keyword">Cost</span>],
          [FullPrice],
          [Price],
          [Quantity],
          [Taxable],
          [SalesTax],
          [TaxAmount],
          [<span class="hljs-keyword">Comment</span>],
          [DateCreated],
          [Notes],
          [ItemLookupCode],
          [SellByWeight],
          [CategoryName],
          [OptionDescription],
          [OptionDescriptionPrice]
        )
        <span class="hljs-keyword">values</span>
        (
          @pkid,
          @<span class="hljs-keyword">ID</span>,
          @OrderID,
          @TransactionNumber,
          @ItemID,
          @Description,
          @<span class="hljs-keyword">Cost</span>,
          @FullPrice,
          @Price,
          @Quantity,
          @Taxable,
          @SalesTax,
          @TaxAmount,
          @<span class="hljs-keyword">Comment</span>,
          @DateCreated,
          @Notes,
          @ItemLookupCode,
          @SellByWeight,
          @CategoryName,
          @OptionDescription,
          @OptionDescriptionPrice
        )
      <span class="hljs-keyword">commit</span> <span class="hljs-keyword">transaction</span> @p_namedtransaction
      <span class="hljs-keyword">select</span> @theGUID <span class="hljs-keyword">as</span> [theGUID] <span class="hljs-comment">-- return this value for cp_APIPOSTransactionEntryExportCL</span>
    <span class="hljs-keyword">end</span> try
    <span class="hljs-keyword">begin</span> catch
      <span class="hljs-keyword">if</span> @@trancount &gt; <span class="hljs-number">0</span>
        <span class="hljs-keyword">rollback</span> <span class="hljs-keyword">transaction</span> @p_namedtransaction;
      throw;
    <span class="hljs-keyword">end</span> catch
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">go</span>
</code></pre>
<p>Key Features:</p>
<ol>
<li>Purpose</li>
</ol>
<ul>
<li><p>Inserts itemized transaction details into the POSTransactionExportCLTransactionEntries table.</p>
</li>
<li><p>Handles granular data, including pricing, taxes, discounts, quantities, and metadata for individual items in an order.</p>
</li>
</ul>
<ol start="2">
<li>Parameters</li>
</ol>
<ul>
<li><p>Transaction Identifiers: @pkid links the entry to the primary transaction, while <a target="_blank" href="https://dev.to/id">@id</a> and @OrderID help identify the item within the context of the transaction.</p>
</li>
<li><p>Item-Specific Details: @ItemID, @Description, @Cost, <a target="_blank" href="https://dev.to/price">@price</a>, @FullPrice, @Taxable, and @Quantity describe the item and its financial attributes.</p>
</li>
<li><p>Additional Metadata: Fields like @Notes, @ItemLookupCode, @CategoryName, and @OptionDescription add context for reporting or customer service.</p>
</li>
</ul>
<ol start="3">
<li>Transactional Integrity</li>
</ol>
<ul>
<li><p>The procedure employs a named transaction to ensure that the insertion is atomic, meaning it is either fully successful or fully rolled back in case of an error.</p>
</li>
<li><p>A unique identifier (@theGUID) is generated and returned for referencing the specific entry.</p>
</li>
</ul>
<ol start="4">
<li>Error Handling</li>
</ol>
<ul>
<li>A TRY...CATCH block ensures that any errors encountered during execution result in a rollback of the transaction, maintaining data integrity.</li>
</ul>
<ol start="5">
<li>Output</li>
</ol>
<ul>
<li>The unique identifier (@theGUID) of the inserted entry is returned, enabling seamless linking and traceability.</li>
</ul>
<ol start="6">
<li>Benefits</li>
</ol>
<ul>
<li><p>Comprehensive Tracking: Captures detailed item-level data for every transaction, crucial for inventory management, tax calculations, and customer service.</p>
</li>
<li><p>Flexibility: The numerous parameters allow the procedure to handle a wide range of item types and transaction scenarios.</p>
</li>
<li><p>Auditability: By linking to the primary transaction (@pkid) and generating unique identifiers, it supports clear and traceable records.</p>
</li>
</ul>
<p>This procedure is a critical component of the POS system, ensuring that all transaction details are recorded and accessible for business operations, analysis, and compliance purposes.</p>
<p><strong>A Note About Using System.Data.SqlClient</strong><br />In this system, I have chosen to use System.Data.SqlClient for database interactions due to its ability to provide high-performance, low-level access to the SQL Server database. This approach is particularly important in scenarios where I need to process large JSON streams efficiently and with minimal latency and minimizes overhead and provides the flexibility required for performance optimization.</p>
<p>While Entity Framework is good for working with databases in higher-level applications, its abstraction layers can introduce unnecessary overhead, which can be a limitation when the goal is to maximize speed, optimization, and resource utilization. By opting for System.Data.SqlClient, I avoid this overhead for more efficient database access and faster processing times - both key factors when dealing with high-performance tasks such as JSON stream processing and data-intensive operations.</p>
<p>In summary, the use of System.Data.SqlClient ensures that the system can handle complex, performance-critical database tasks with greater precision and speed, allowing for the optimization of stored procedure execution and overall system responsiveness.</p>
<p><strong>Conclusion</strong><br />Developing a custom webhook to process a client-supplied POS JSON stream required a strategic approach to ensure efficiency, accuracy, and security. By breaking down the task into manageable stages - from webhook setup and model definition to SQL database design and stored procedure creation, I was able to deliver a robust solution that seamlessly integrates with the client's data stream.</p>
<p>Using strongly typed C# model classes and carefully crafted SQL structures, the implementation ensures data consistency and simplifies debugging. Additionally, the modular design of the webhook also provides a foundation for extending functionality and supporting additional future integrations.</p>
]]></content:encoded></item></channel></rss>