<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dhana</title>
    <description>The latest articles on DEV Community by Dhana (@dhanagani_lakshmi_2487ad0).</description>
    <link>https://dev.to/dhanagani_lakshmi_2487ad0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4114244%2Fe294e2fe-04e7-4575-9208-f6075df3783e.jpg</url>
      <title>DEV Community: Dhana</title>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dhanagani_lakshmi_2487ad0"/>
    <language>en</language>
    <item>
      <title>When the Commit Succeeds but the Response Is Lost: Do You Always Need Operation IDs?</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:21:50 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/when-the-commit-succeeds-but-the-response-is-lost-do-you-always-need-operation-ids-l7n</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/when-the-commit-succeeds-but-the-response-is-lost-do-you-always-need-operation-ids-l7n</guid>
      <description>&lt;p&gt;An earlier article looked at the gap between what a UI shows and what a database actually commits: a screen that announces "Step 1 saved" before a later step fails and the whole transaction rolls back. A reader then pointed out the mirror-image problem, which is just as important. The commit can succeed while the response never reaches the user. The user sees a timeout, assumes the save failed, and tries again, even though the database already changed.&lt;/p&gt;

&lt;p&gt;This article works through that scenario, the fix the reader suggested, and what I found when I checked it against a real monthly batch process.&lt;/p&gt;

&lt;p&gt;The Scenario&lt;/p&gt;

&lt;p&gt;A salary batch screen performs its save in three steps:&lt;/p&gt;

&lt;p&gt;Insert the batch record&lt;br&gt;
Update the related records&lt;br&gt;
Commit, which makes everything permanent&lt;/p&gt;

&lt;p&gt;Consider a timeout that happens after step 3:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
transaction.Commit();          // succeeds, data is now permanent&lt;br&gt;
return Ok("Batch processed");  // this response never reaches the user&lt;/p&gt;

&lt;p&gt;The database is correct. The user's screen says "request timed out." Nothing in the transaction logic was wrong, yet the user now believes the save failed, and the natural reaction is to run it again. In payroll, running a batch twice is exactly the kind of thing that is expensive to untangle later.&lt;/p&gt;

&lt;p&gt;The Suggested Fix&lt;/p&gt;

&lt;p&gt;The reader's approach has three parts:&lt;/p&gt;

&lt;p&gt;A stable operation ID. Each save request carries a unique ID generated before sending. A retry carries the same ID.&lt;br&gt;
An idempotent command. Handling the same operation twice has the same effect as handling it once.&lt;br&gt;
A status endpoint backed by durable state. If the user is unsure what happened, the UI asks the server about that operation ID and gets a definite answer instead of guessing.&lt;/p&gt;

&lt;p&gt;The server keeps a record of every operation ID it has handled. When a retry arrives, it checks the record:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var existing = operationStore.Find(operationId);&lt;/p&gt;

&lt;p&gt;if (existing != null)&lt;br&gt;
{&lt;br&gt;
    return Ok(existing.Result); // already handled, return the original outcome&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;// otherwise process normally, then record the operation and its result&lt;/p&gt;

&lt;p&gt;The retry after a timeout no longer causes a second run. The user simply receives the result of the first attempt. The record must be stored durably, in a table rather than in memory, because an in-memory record disappears if the server restarts between the first attempt and the retry. For workflows that also publish events or notifications, an outbox table serves a related purpose, recording the intent to notify in the same transaction as the data change so it can be delivered reliably afterward.&lt;/p&gt;

&lt;p&gt;This is a solid design, and for high-volume systems where retries are frequent it is the right answer.&lt;/p&gt;

&lt;p&gt;What My Own System Already Did&lt;/p&gt;

&lt;p&gt;When I walked through the same scenario for our salary generation screen, I found something the general advice doesn't account for. Salary generation runs once a month, by people who plan for it and are careful with it. And after a successful commit, if someone retries, the screen doesn't process the batch again. It shows "no data found."&lt;/p&gt;

&lt;p&gt;The reason is that there is nothing left to process. The first run changed the state of the data, so a second attempt finds no pending records. The saved data itself acts as the guard. That is a legitimate form of idempotency, achieved through state rather than through operation IDs:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var pending = GetPendingRecords(batchPeriod);&lt;/p&gt;

&lt;p&gt;if (pending.Count == 0)&lt;br&gt;
{&lt;br&gt;
    // nothing left to process&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;For the dangerous part of the problem, duplicate processing after a lost response, this design already holds. No operation ID table was needed.&lt;/p&gt;

&lt;p&gt;The Gap That Remained&lt;/p&gt;

&lt;p&gt;The guard prevents duplicates, but it leaves a communication problem. After a timeout, "no data found" is ambiguous. It could mean:&lt;/p&gt;

&lt;p&gt;the batch was already processed successfully and nothing is left to do&lt;br&gt;
there was never anything to process for that period&lt;br&gt;
something went wrong&lt;/p&gt;

&lt;p&gt;A user who retries after a lost response and sees only "no data found" cannot tell which of these is true, and in payroll, doubt about whether a run went through is a real cost.&lt;/p&gt;

&lt;p&gt;The fix requires no new infrastructure. Instead of reporting an empty result, check whether the batch is already marked as processed and say so:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
if (pending.Count == 0)&lt;br&gt;
{&lt;br&gt;
    var processedOn = GetProcessedDate(batchPeriod);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (processedOn != null)
{
    return Ok($"This batch was already processed on {processedOn:d}.");
}

return Ok("No pending records found for this period.");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Now the two situations produce different messages, and the user who retries after a timeout gets a clear answer to the question they actually have: did it work?&lt;/p&gt;

&lt;p&gt;Choosing Between the Two Approaches&lt;/p&gt;

&lt;p&gt;The lesson is less about which approach is better and more about matching the machinery to the situation.&lt;/p&gt;

&lt;p&gt;Operation IDs, durable operation records, and status endpoints earn their cost when operations are frequent, retries are common, and the data doesn't naturally reveal whether an operation already ran. A state-based guard, combined with a message that reports the true state, can be enough when an operation is infrequent, tightly controlled, and leaves clear evidence in the data once it has run.&lt;/p&gt;

&lt;p&gt;A useful question to ask of any multi-step save is: if the response is lost after the commit, what stops a retry from repeating the work, and what does the user see when they retry? If the data state already answers the first part, the remaining work may be a clearer message rather than a new subsystem.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;A committed transaction and a delivered response are two separate events, and either one can fail without the other. Operation IDs make the retry safe by recognizing repeated requests. In some systems, the saved data already does that job, and the gap that remains is in what the user is told. Checking which case you are in before adding infrastructure is worth the few minutes it takes.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>database</category>
      <category>trans</category>
      <category>csharp</category>
    </item>
    <item>
      <title>The Gap Between What the UI Shows and What the Database Actually Commits</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Sat, 26 Sep 2026 10:02:47 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/the-gap-between-what-the-ui-shows-and-what-the-database-actually-commits-klo</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/the-gap-between-what-the-ui-shows-and-what-the-database-actually-commits-klo</guid>
      <description>&lt;p&gt;A transaction wrapping multiple database operations guarantees that the data ends up consistent — either everything commits, or everything rolls back. What it says nothing about is what the screen shows the person using the application while that transaction is in progress. A reader's question on a previous article about the Unit of Work pattern surfaced exactly this gap, and it's worth examining carefully, because it's a genuinely separate concern from transactional correctness at the database level.&lt;/p&gt;

&lt;p&gt;The Question That Prompted This&lt;/p&gt;

&lt;p&gt;On a salary batch processing screen using a shared transaction across two save steps, a reader asked: does the UI show a single save failure when the second write fails, or can it make the first step look successful before the rollback happens? For anyone reconciling payroll later, that distinction matters enormously.&lt;/p&gt;

&lt;p&gt;Why This Is a Genuinely Different Problem&lt;/p&gt;

&lt;p&gt;The database-level guarantee is straightforward and, if built correctly, reliable:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var batchResult = InsertBatchRecord(batchModel, connection, transaction);&lt;/p&gt;

&lt;p&gt;if (batchResult.Success)&lt;br&gt;
{&lt;br&gt;
    var updateResult = UpdateRelatedRecords(batchResult.GeneratedId, relatedModel, connection, transaction);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (updateResult.Success)
{
    transaction.Commit();
}
else
{
    transaction.Rollback();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;If UpdateRelatedRecords fails, transaction.Rollback() undoes everything, including the batch insert that technically succeeded moments earlier. The database ends up correct either way.&lt;/p&gt;

&lt;p&gt;But consider what happens if the code also updates the UI at each step along the way:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var batchResult = InsertBatchRecord(batchModel, connection, transaction);&lt;/p&gt;

&lt;p&gt;if (batchResult.Success)&lt;br&gt;
{&lt;br&gt;
    DisplayMessage("Step 1: Batch record saved successfully!"); // shown immediately&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;var updateResult = UpdateRelatedRecords(batchResult.GeneratedId, relatedModel, connection, transaction);

if (!updateResult.Success)
{
    transaction.Rollback();
    DisplayMessage("Step 2 failed. Transaction rolled back.");
}
else
{
    transaction.Commit();
    DisplayMessage("Both steps completed successfully.");
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The person watching this screen sees "Step 1: Batch record saved successfully!" before Step 2 has even run. If Step 2 then fails and the rollback executes, the database correctly ends up with no batch record at all — but the user already saw, and may have acted on, a success message for data that no longer exists. Even if a second message eventually clarifies that the transaction rolled back, that first message was real and displayed, and if the person only glanced at the screen once, or the message scrolled past, or they're reviewing a screenshot or log entry taken at that moment, they have genuine evidence of a "success" that the database has since erased.&lt;/p&gt;

&lt;p&gt;Why This Matters Specifically for Reconciliation&lt;/p&gt;

&lt;p&gt;The reader's framing — "for whoever is reconciling payroll" — points at exactly why this distinction has real consequences. Reconciliation often happens after the fact, using whatever evidence is available: screenshots, support tickets, log entries, or someone's memory of what they saw on screen. If the UI displayed an intermediate success message that the database later invalidated through a rollback, that piece of evidence is now actively misleading anyone trying to reconstruct what actually happened. A support ticket referencing "the batch was saved, I saw the confirmation" becomes hard to reconcile against a database that shows no such record — not because anyone made an error, but because the UI communicated something that was only momentarily, provisionally true.&lt;/p&gt;

&lt;p&gt;The Safer Pattern: One Final Status, Not Per-Step Messages&lt;/p&gt;

&lt;p&gt;The straightforward fix is to withhold any success messaging until the entire transaction has actually committed:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var batchResult = InsertBatchRecord(batchModel, connection, transaction);&lt;br&gt;
bool overallSuccess = false;&lt;/p&gt;

&lt;p&gt;if (batchResult.Success)&lt;br&gt;
{&lt;br&gt;
    var updateResult = UpdateRelatedRecords(batchResult.GeneratedId, relatedModel, connection, transaction);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (updateResult.Success)
{
    transaction.Commit();
    overallSuccess = true;
}
else
{
    transaction.Rollback();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
else&lt;br&gt;
{&lt;br&gt;
    transaction.Rollback();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;DisplayMessage(overallSuccess&lt;br&gt;
    ? "Batch processed successfully."&lt;br&gt;
    : "Batch processing failed. No changes were saved.");&lt;/p&gt;

&lt;p&gt;No intermediate messages are shown at all. The person only sees one final status, generated only after the commit or rollback has genuinely happened. This removes the possibility of a UI message ever describing a state that a subsequent rollback later invalidates — the message is only ever displayed once the underlying data is in its final, permanent state.&lt;/p&gt;

&lt;p&gt;When Progress Indicators Are Still Needed&lt;/p&gt;

&lt;p&gt;Some workflows genuinely need to show progress during a multi-step operation, particularly if steps take noticeable time. In those cases, progress indicators should communicate activity, not completion — something like "Processing step 1 of 2..." rather than "Step 1 saved successfully," since the former makes no claim about permanence that a rollback could later contradict, while the latter does.&lt;/p&gt;

&lt;p&gt;A Practical Review Question&lt;/p&gt;

&lt;p&gt;When reviewing any multi-step save operation, it's worth asking not only "does the database stay consistent on failure," but separately, "does any user-facing message get shown before the transaction actually resolves, and could that message become misleading if a later step fails?" These are two different checks, and passing the first doesn't guarantee passing the second — a perfectly correct transaction can still sit behind a UI that tells a story the database doesn't ultimately support.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;Transactional correctness at the database level and accurate communication at the UI level are related but distinct problems. A shared transaction with proper commit and rollback logic guarantees the data ends up consistent, but it says nothing about what gets displayed to a user in the moments before that resolution happens. The safer default is to withhold any success messaging until the entire operation has genuinely committed, so that whatever the user sees — and whatever gets captured in a screenshot, log, or support ticket — accurately reflects the database's final, permanent state, rather than a provisional condition a rollback might later erase.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>database</category>
      <category>uidesign</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Recognizing the Unit of Work Pattern in a Simple Multi-Step Save</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Sat, 26 Sep 2026 04:54:29 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/recognizing-the-unit-of-work-pattern-in-a-simple-multi-step-save-lm9</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/recognizing-the-unit-of-work-pattern-in-a-simple-multi-step-save-lm9</guid>
      <description>&lt;p&gt;Some patterns don't announce themselves with an obvious interface or a textbook-matching class name. Sometimes they're just... there, quietly, in code that looks like ordinary sequential logic. This article walks through a real example: a multi-step save operation that turns out to be a working instance of the Unit of Work pattern, even though nothing in the code is labeled that way.&lt;/p&gt;

&lt;p&gt;The Code in Question&lt;/p&gt;

&lt;p&gt;A salary/batch processing screen performs its save in two steps, calling two separate methods:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var batchResult = InsertBatchRecord(batchModel, connection, transaction);&lt;/p&gt;

&lt;p&gt;if (batchResult.Success)&lt;br&gt;
{&lt;br&gt;
    var updateResult = UpdateRelatedRecords(batchResult.GeneratedId, relatedModel, connection, transaction);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Two details stand out immediately: both calls share the exact same connection and transaction objects, and the second call only executes if the first one succeeded.&lt;/p&gt;

&lt;p&gt;What's Actually Happening Here&lt;/p&gt;

&lt;p&gt;Sharing the same connection and transaction across both calls means these two database operations aren't independent of each other — they're bound together as a single logical unit. Neither InsertBatchRecord nor UpdateRelatedRecords opens its own separate connection or starts its own separate transaction; they both operate within one shared transactional context established somewhere earlier in the calling code.&lt;/p&gt;

&lt;p&gt;The conditional check (if (batchResult.Success)) means the second operation is deliberately gated on the first one's outcome. This isn't accidental sequencing — it's a guard against doing the second write if the first one didn't succeed.&lt;/p&gt;

&lt;p&gt;Put together, this is the essential shape of the Unit of Work pattern: treating multiple related database operations as a single, all-or-nothing unit, so that a partial failure doesn't leave the database in an inconsistent state.&lt;/p&gt;

&lt;p&gt;Why This Matters: What Happens on Failure&lt;/p&gt;

&lt;p&gt;The real value of this structure shows up specifically when something goes wrong. Consider what should happen if UpdateRelatedRecords fails after InsertBatchRecord already succeeded within the same transaction:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
try&lt;br&gt;
{&lt;br&gt;
    var batchResult = InsertBatchRecord(batchModel, connection, transaction);&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (batchResult.Success)
{
    var updateResult = UpdateRelatedRecords(batchResult.GeneratedId, relatedModel, connection, transaction);

    if (updateResult.Success)
    {
        transaction.Commit();
    }
    else
    {
        transaction.Rollback();
    }
}
else
{
    transaction.Rollback();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;br&gt;
catch (Exception)&lt;br&gt;
{&lt;br&gt;
    transaction.Rollback();&lt;br&gt;
    throw;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Because both operations share the same transaction object, a rollback at any point undoes everything done within that transaction so far — including the batch record insert that technically succeeded moments earlier. Without this shared-transaction structure, a failure in the second step would leave the first step's data permanently saved, with no corresponding update — an inconsistent, partially-completed state that could be difficult to detect and even harder to clean up later, especially in a payroll context where a stray batch record without its related update could cause real downstream confusion.&lt;/p&gt;

&lt;p&gt;Why This Is Easy to Overlook as "Just a Pattern"&lt;/p&gt;

&lt;p&gt;Code like this often doesn't get recognized as implementing a named pattern, because it doesn't involve an interface, a dedicated UnitOfWork class, or any explicit pattern-related vocabulary. It's simply two method calls sharing a connection and transaction, with a success check between them. But the underlying intent — grouping related operations so they succeed or fail together — is precisely what the Unit of Work pattern describes, regardless of whether the code uses that terminology or a more formal implementation involving a dedicated coordinating class.&lt;/p&gt;

&lt;p&gt;Formal implementations of Unit of Work often introduce a dedicated class responsible for tracking all pending changes and committing them together:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class UnitOfWork&lt;br&gt;
{&lt;br&gt;
    private readonly OracleTransaction _transaction;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public bool InsertBatchRecord(BatchModel model) { /* uses _transaction */ }
public bool UpdateRelatedRecords(int batchId, RelatedModel model) { /* uses _transaction */ }

public void Commit() =&amp;gt; _transaction.Commit();
public void Rollback() =&amp;gt; _transaction.Rollback();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;This formalized version makes the pattern more explicit and reusable across different save operations, but the simpler version — passing a shared connection and transaction directly into sequential method calls — achieves the same core guarantee for a single, specific save operation. It's a smaller-scale, less formalized instance of the same underlying idea.&lt;/p&gt;

&lt;p&gt;A Practical Question for Reviewing Multi-Step Saves&lt;/p&gt;

&lt;p&gt;When reviewing any code that performs more than one database write as part of a single logical operation, it's worth asking: if the second (or third, or later) write fails, what happens to the writes that already succeeded? If the honest answer is "they stay saved, creating a partial, inconsistent result," that's a signal the operation needs a shared transaction wrapping all the steps together — exactly the structure already present in this example.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;The Unit of Work pattern doesn't require a dedicated class or explicit pattern-vocabulary to be genuinely present in code. Sharing a single connection and transaction across multiple related database operations, combined with a rollback on any failure, is the pattern's essential behavior, however plainly it's written. Recognizing it in ordinary-looking sequential code — rather than only in formalized, textbook implementations — makes it easier to spot both where the pattern is already protecting data consistency, and where a multi-step save might be missing that protection entirely.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>database</category>
      <category>csharp</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Interface for Testability? A Reader's Pushback Made Me Reconsider</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Fri, 25 Sep 2026 11:51:01 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/interface-for-testability-a-readers-pushback-made-me-reconsider-1ll4</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/interface-for-testability-a-readers-pushback-made-me-reconsider-1ll4</guid>
      <description>&lt;p&gt;A previous article argued that registering a concrete class instead of an interface in Dependency Injection quietly determines whether a class can be unit tested. A reader, Ivan Rossouw, pushed back with a genuinely useful correction: framing an interface as the condition for testability oversimplifies things. This article works through that correction properly, because it's a distinction worth understanding precisely, not glossing over.&lt;/p&gt;

&lt;p&gt;The Original Claim&lt;/p&gt;

&lt;p&gt;The earlier argument was straightforward: if a Controller depends on a concrete class directly,&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public WorkFlowController(clsWorkFlow workFlowDataAccess)&lt;/p&gt;

&lt;p&gt;there's no way to substitute a fake implementation during testing, because there's no interface for a fake to implement. Register against an interface instead,&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public WorkFlowController(IclsWorkFlow workFlowDataAccess)&lt;/p&gt;

&lt;p&gt;and a fake IclsWorkFlow can stand in for the real, database-backed clsWorkFlow during a unit test.&lt;/p&gt;

&lt;p&gt;This is true as far as it goes. What it leaves out is that "testable" doesn't only mean "mockable through an interface."&lt;/p&gt;

&lt;p&gt;The Correction: A Concrete Class Can Be Tested Directly&lt;/p&gt;

&lt;p&gt;A concrete class, with no interface at all, can still be tested — just differently:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var realService = new clsWorkFlow(testConnectionString);&lt;br&gt;
var result = realService.GetAddWorkFlows(employeeSlno: 5, createdBy: 1);&lt;br&gt;
Assert.Equal(expectedCount, result.Count);&lt;/p&gt;

&lt;p&gt;This test genuinely runs clsWorkFlow's actual logic, including its real database calls. It's a legitimate test — it's simply a different kind of test than a unit test that isolates logic through a fake dependency. This is called an integration test: it verifies that the class works correctly together with a real (or real-like) database, rather than verifying the class's logic in isolation from everything it depends on.&lt;/p&gt;

&lt;p&gt;Why an Integration Test Can Be More Valuable Here&lt;/p&gt;

&lt;p&gt;For a class like clsWorkFlow, whose entire responsibility is talking to Oracle — building commands, passing parameters, mapping results — a mock-based unit test has a real limitation: a mock can't catch a genuine SQL or parameter-binding mistake, because a mock never touches a real database at all. If a stored procedure name is wrong, or a parameter type doesn't match what Oracle expects, a unit test built entirely on a fake IclsWorkFlow will pass without ever discovering the problem — because the fake never runs any real database logic.&lt;/p&gt;

&lt;p&gt;An integration test, run against a disposable database — a temporary, throwaway database set up specifically for testing and torn down afterward, not the real production database — would catch exactly this kind of error, because it actually executes the real SQL against a real (if temporary) target. For a "thin adapter" class whose main job is translating between application code and database calls, this kind of test can surface more genuine, useful failures than a unit test that mocks the very thing it's supposed to be verifying.&lt;/p&gt;

&lt;p&gt;A More Precise Rule of Thumb&lt;/p&gt;

&lt;p&gt;Ivan's suggested approach, worth adopting as a working rule: introduce an interface around volatile or external behavior — code that reaches outside the application's own process, like a database call, an external API, or a file system operation — and keep genuinely simple, self-contained services concrete, without an interface just for the sake of having one.&lt;/p&gt;

&lt;p&gt;The reasoning: an interface's real value isn't testability in the abstract — it's the ability to substitute behavior when that behavior is expensive, slow, unreliable, or external to the code being tested. A trivial service with no external dependencies doesn't need an interface to be tested; it can simply be instantiated directly, the same way clsWorkFlow can be tested directly against a disposable database.&lt;/p&gt;

&lt;p&gt;A Useful Review Question&lt;/p&gt;

&lt;p&gt;A practical check worth applying when reviewing whether an interface actually earns its place: does this interface express a genuine capability, or does it just duplicate every method on one class?&lt;/p&gt;

&lt;p&gt;An interface like IclsWorkFlow, which exists specifically to allow swapping the real Oracle-backed implementation for a fake one during testing (or, in principle, for a different database implementation later), expresses a real capability — "something that can fetch and save workflow data" — independent of any one specific implementation. An interface that exists purely because "best practice says register interfaces," without any real intention of ever substituting a different implementation, is closer to unnecessary duplication than genuine abstraction.&lt;/p&gt;

&lt;p&gt;What This Changes About the Original Argument&lt;/p&gt;

&lt;p&gt;The core point — that concrete-class registration limits how a Controller can be tested through mocking — still holds. What needed correcting was the implication that mocking through an interface is the only legitimate path to testing, or automatically the best one. For a thin, database-facing class, a well-designed integration test against a disposable database can be more informative than a unit test built on a mock that can never fail the way real database code actually fails.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;Testability isn't a single, uniform requirement satisfied only by interfaces and mocks. A concrete class can be genuinely well-tested through integration testing against a disposable resource, and for classes whose entire purpose is interacting with an external system, this can catch real problems that a mock-based unit test structurally cannot. The more precise habit isn't "always use an interface for testability" — it's introducing interfaces deliberately around volatile or external behavior, and asking, for each one, whether it expresses a real, substitutable capability or merely mirrors a single class's method list.&lt;/p&gt;

</description>
      <category>unittest</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>testing</category>
    </item>
    <item>
      <title>Design Patterns in a Real Project: One I Actually Had, and One I Thought I Had But Didn't</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Thu, 24 Sep 2026 09:55:41 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/design-patterns-in-a-real-project-one-i-actually-had-and-one-i-thought-i-had-but-didnt-2kp9</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/design-patterns-in-a-real-project-one-i-actually-had-and-one-i-thought-i-had-but-didnt-2kp9</guid>
      <description>&lt;p&gt;Design pattern articles often present clean, already-perfect examples: here's the pattern, here's the code, here's why it's elegant. Real codebases are messier than that, and recognizing a pattern accurately — including recognizing when something isn't actually a pattern, just plain code that happens to look similar — is a more useful skill than memorizing textbook definitions. This article walks through two patterns I went looking for in an actual enterprise .NET project: one I genuinely had, and one I initially thought I had, until a closer look showed I didn't.&lt;/p&gt;

&lt;p&gt;Pattern One: The Repository Pattern, Already There Without the Name&lt;/p&gt;

&lt;p&gt;The project in question uses a familiar structure: a Controller layer, and a Data Access layer accessed through an interface.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public interface IclsWorkFlow&lt;br&gt;
{&lt;br&gt;
    List GetAddWorkFlows(int employeeSlno, int createdBy);&lt;br&gt;
    bool SaveAddWorkFlows(List models);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;public class clsWorkFlow : IclsWorkFlow&lt;br&gt;
{&lt;br&gt;
    public List GetAddWorkFlows(int employeeSlno, int createdBy)&lt;br&gt;
    {&lt;br&gt;
        // ADO.NET / Oracle-specific logic lives here&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public bool SaveAddWorkFlows(List&amp;lt;UserRoleMappingModelDto&amp;gt; models)
{
    // ADO.NET / Oracle-specific logic lives here
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The Controller depends only on IclsWorkFlow, never touching clsWorkFlow or any ADO.NET code directly:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class WorkFlowController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    private readonly IclsWorkFlow _workFlowDataAccess;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public WorkFlowController(IclsWorkFlow workFlowDataAccess)
{
    _workFlowDataAccess = workFlowDataAccess;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;This is a Repository Pattern, even though nothing in the codebase is named "Repository." The naming convention here (cls prefix, following the project's existing style) is different from the textbook IEmployeeRepository / EmployeeRepository naming, but the structural intent is identical: hide database-specific implementation details behind a clean interface, so the rest of the application depends on a contract rather than a concrete data-access mechanism. The flow is straightforward — UI, through a thin Controller acting as the business logic layer, calling the IclsWorkFlow interface, which resolves to clsWorkFlow handling the actual ADO.NET calls.&lt;/p&gt;

&lt;p&gt;Recognizing this mattered less for adding anything new, and more for realizing the project already followed a sound, testable structure — the pattern was present in substance, just not in naming convention.&lt;/p&gt;

&lt;p&gt;Pattern Two: Where I Was Wrong About Template Method&lt;/p&gt;

&lt;p&gt;A separate part of the project has a shared base class for different request types — leave requests, advance requests — each inheriting common fields and an approval method:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
class BaseRequest&lt;br&gt;
{&lt;br&gt;
    public string EmpID;&lt;br&gt;
    public int Status;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public void ApproveLevel(int currentSeqNo, int nextSeqNo)
{
    Status = (currentSeqNo == nextSeqNo) ? 1 : 0;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;class LeaveRequest : BaseRequest&lt;br&gt;
{&lt;br&gt;
    public DateTime FromDate;&lt;br&gt;
    public DateTime ToDate;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;My first instinct was to call this a Template Method Pattern — a base class defining a process, subclasses customizing pieces of it. On closer inspection, that label doesn't actually fit. ApproveLevel() is a single method with fixed logic, not a defined sequence of multiple steps, and LeaveRequest doesn't override or customize any part of it. This is plain Inheritance: shared fields and one shared method, reused by subclasses that add their own unrelated fields. Calling it Template Method would have been applying the label to code that doesn't actually meet the pattern's requirements.&lt;/p&gt;

&lt;p&gt;What Would Actually Make It a Template Method Pattern&lt;/p&gt;

&lt;p&gt;The distinguishing requirement for Template Method is a defined sequence of steps in the base class, where individual steps — not the overall order — are customized by subclasses:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
abstract class BaseRequest&lt;br&gt;
{&lt;br&gt;
    // The "template" - a fixed sequence, defined once, never overridden&lt;br&gt;
    public void ProcessRequest()&lt;br&gt;
    {&lt;br&gt;
        ValidateRequest();&lt;br&gt;
        CheckApprovalLevel();&lt;br&gt;
        SendNotification();&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;protected virtual void ValidateRequest()
{
    // default shared validation, subclasses may override
}

protected void CheckApprovalLevel()
{
    // shared approval logic, same for every request type
}

protected abstract void SendNotification();
// forces each subclass to implement its own version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;class LeaveRequest : BaseRequest&lt;br&gt;
{&lt;br&gt;
    protected override void SendNotification()&lt;br&gt;
    {&lt;br&gt;
        // "Your leave from X to Y is approved"&lt;br&gt;
    }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;class AdvanceRequest : BaseRequest&lt;br&gt;
{&lt;br&gt;
    protected override void SendNotification()&lt;br&gt;
    {&lt;br&gt;
        // "Your advance of X is approved"&lt;br&gt;
    }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;With ProcessRequest() defining the fixed sequence — validate, then check approval, then notify — and only SendNotification() varying per subclass, this genuinely qualifies. The overall algorithm's shape is locked in the base class; only specific steps are customizable. That's the actual requirement, and it's a meaningfully different structure from simply sharing a field and a method across subclasses.&lt;/p&gt;

&lt;p&gt;Why the Distinction Is Worth Making Carefully&lt;/p&gt;

&lt;p&gt;It would have been easy to write "we use the Template Method Pattern here" in a design document or a technical interview answer, based on surface similarity — inheritance, a shared method, subclasses. But applying a pattern name to code that doesn't structurally satisfy the pattern's actual definition creates a false sense of design sophistication, and can mislead a teammate or reviewer who takes the label at face value and expects a defined multi-step algorithm that isn't actually there. Recognizing plain Inheritance as plain Inheritance, rather than reaching for a more impressive-sounding pattern name, is a more honest and more useful skill than pattern-matching on surface features.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;Design patterns are most useful when they accurately describe a structure already serving a real purpose, not when they're retrofitted onto code because the shape looks vaguely similar. A Repository Pattern can exist in a codebase without ever using the word "Repository," as long as the underlying structure — interface-based abstraction over data access — is genuinely there. Conversely, code that merely shares fields and a method through inheritance isn't a Template Method Pattern unless it defines an actual sequence of steps with specific points designed for customization. Being precise about which is which, even when it means concluding "this is just inheritance, not a named pattern," is a more valuable habit than confidently mislabeling code either way.&lt;/p&gt;

</description>
      <category>designpatterns</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Registering a Concrete Class Instead of an Interface: A Small DI Detail With a Big Testing Cost</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Wed, 23 Sep 2026 04:08:55 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/registering-a-concrete-class-instead-of-an-interface-a-small-di-detail-with-a-big-testing-cost-4da3</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/registering-a-concrete-class-instead-of-an-interface-a-small-di-detail-with-a-big-testing-cost-4da3</guid>
      <description>&lt;p&gt;Dependency Injection registration often gets reviewed for one thing: which lifetime was chosen — Singleton, Scoped, or Transient. A separate, easy-to-miss detail matters just as much for the long-term health of a codebase: whether the registration points to an interface or a concrete class. This distinction rarely causes a visible bug on its own, but it quietly determines whether a class can ever be properly unit tested.&lt;/p&gt;

&lt;p&gt;Two Registrations That Look Similar But Aren't&lt;/p&gt;

&lt;p&gt;Consider these two ways of registering the same underlying class:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
// Registration A - concrete class only&lt;br&gt;
services.AddTransient();&lt;/p&gt;

&lt;p&gt;// Registration B - interface mapped to implementation&lt;br&gt;
services.AddScoped();&lt;/p&gt;

&lt;p&gt;Both successfully let clsWorkFlow be injected somewhere in the application. Both compile without error, and both work correctly at runtime for normal request handling. The difference only becomes visible when you try to do something with the class beyond its default, real-database behavior — specifically, when you try to test it.&lt;/p&gt;

&lt;p&gt;What the Consuming Controller Looks Like in Each Case&lt;/p&gt;

&lt;p&gt;Registration A typically pairs with a Controller like this:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class WorkFlowController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    private readonly clsWorkFlow _workFlowDataAccess;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public WorkFlowController(clsWorkFlow workFlowDataAccess)
{
    _workFlowDataAccess = workFlowDataAccess;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Registration B pairs with this instead:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class WorkFlowController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    private readonly IclsWorkFlow _workFlowDataAccess;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public WorkFlowController(IclsWorkFlow workFlowDataAccess)
{
    _workFlowDataAccess = workFlowDataAccess;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;At a glance, these look almost identical — one word different in two places. But that one word changes what's possible during testing.&lt;/p&gt;

&lt;p&gt;Why the Interface Version Can Be Tested and the Concrete Version Can't&lt;/p&gt;

&lt;p&gt;To unit test a Controller in isolation — without hitting a real Oracle database — you need to supply a fake or mock version of whatever it depends on. This is only possible when the dependency is expressed as an interface:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class FakeWorkFlow : IclsWorkFlow&lt;br&gt;
{&lt;br&gt;
    public List GetAddWorkFlows(int employeeSlno, int createdBy)&lt;br&gt;
    {&lt;br&gt;
        return new List&lt;br&gt;
        {&lt;br&gt;
            new CtrlSlnoName { CtrlSlno = 1, Name = "Test Workflow" }&lt;br&gt;
        };&lt;br&gt;
    }&lt;br&gt;
    // remaining interface methods implemented with fake data&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;With the Controller depending on IclsWorkFlow, a test can substitute FakeWorkFlow in place of the real, database-backed clsWorkFlow:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
var controller = new WorkFlowController(new FakeWorkFlow());&lt;br&gt;
var result = controller.GetAddRoleMapping(employeeSlno: 5, createdBy: 1);&lt;br&gt;
// assert on result, no real database touched&lt;/p&gt;

&lt;p&gt;If the Controller instead depends directly on the concrete clsWorkFlow class, there is no interface to implement a fake version of. FakeWorkFlow can't be substituted in, because the Controller's constructor demands the actual clsWorkFlow type specifically, not anything that merely behaves like it. The only way to test this Controller is to run it against a real (or real-like) Oracle connection — slower, harder to set up reliably, and prone to failing for reasons unrelated to the actual logic being tested, like a database being temporarily unreachable.&lt;/p&gt;

&lt;p&gt;Why This Detail Is Easy to Miss in Practice&lt;/p&gt;

&lt;p&gt;Registering a concrete class directly is often the path of least resistance early in a project — it compiles, it works, and the missing interface doesn't cause any errors or warnings. The cost only becomes apparent later, when someone tries to add tests to a Controller and discovers there's no way to isolate it from the database. At that point, retrofitting an interface onto an already-widely-used concrete class means touching every registration and every constructor that depends on it — a much larger change than defining the interface correctly from the start would have been.&lt;/p&gt;

&lt;p&gt;This is also why interface-based registration is considered a default good practice in ASP.NET Core projects, even when no tests exist yet at the time a class is written. The interface costs very little to add upfront — a few lines defining method signatures — but keeps the door open for testing later, without requiring a disruptive refactor across the codebase.&lt;/p&gt;

&lt;p&gt;A Practical Habit for Reviewing Registrations&lt;/p&gt;

&lt;p&gt;When reviewing or writing a new service registration, it's worth checking two things together rather than just one: not only which lifetime was chosen, but whether the registration maps an interface to an implementation, or registers a concrete type on its own. A registration like services.AddTransient() works today, but it's worth asking whether this class will ever need to be tested in isolation, mocked, or swapped for an alternative implementation later. If the answer is plausibly yes — which is true for most Data Access classes — defining and registering against an interface from the outset avoids a more expensive change down the line.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;The difference between services.AddTransient() and services.AddScoped() looks trivial on the surface, but it determines whether a class can ever be properly unit tested without touching a real database. Interface-based registration doesn't change how the application behaves for a real user, but it directly shapes how maintainable and testable the codebase remains as it grows — a detail worth checking in every registration, not just the lifetime chosen alongside it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>dependencyinversion</category>
      <category>csharp</category>
      <category>unittest</category>
    </item>
    <item>
      <title>Singleton, Scoped, or Transient: Why DI Lifetime Choice Is the Same Bug You Already Know</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Mon, 21 Sep 2026 09:02:35 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/singleton-scoped-or-transient-why-di-lifetime-choice-is-the-same-bug-you-already-know-1b4e</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/singleton-scoped-or-transient-why-di-lifetime-choice-is-the-same-bug-you-already-know-1b4e</guid>
      <description>&lt;p&gt;Dependency Injection in ASP.NET Core is often taught as a simple mechanical pattern: register an interface, request it in a constructor, the framework wires it up. What's frequently glossed over is that choosing the wrong lifetime for a registered service can silently reproduce the exact same class of bug that shows up in completely unrelated contexts — including the classic ASP.NET Session multi-tab problem many developers eventually encounter. Understanding the connection between these two makes both concepts easier to reason about.&lt;/p&gt;

&lt;p&gt;The Three Lifetimes, Briefly&lt;/p&gt;

&lt;p&gt;ASP.NET Core's built-in DI container offers three lifetime options when registering a service:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
// One instance for the entire application's lifetime&lt;br&gt;
services.AddSingleton();&lt;/p&gt;

&lt;p&gt;// One instance per HTTP request&lt;br&gt;
services.AddScoped();&lt;/p&gt;

&lt;p&gt;// A new instance every single time it's requested&lt;br&gt;
services.AddTransient();&lt;/p&gt;

&lt;p&gt;For a Controller like this:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class WorkFlowController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    private readonly IclsWorkFlow _workFlowDataAccess;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public WorkFlowController(IclsWorkFlow workFlowDataAccess)
{
    _workFlowDataAccess = workFlowDataAccess;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;The Controller never writes new clsWorkFlow() itself — the DI container creates and supplies the instance automatically, based on how it was registered. But which lifetime was chosen for that registration has real consequences that only surface under concurrent load.&lt;/p&gt;

&lt;p&gt;The Scenario: Two Users, One Instance&lt;/p&gt;

&lt;p&gt;Imagine clsWorkFlow is registered as a Singleton. There is now exactly one instance of this class for the entire running application — shared by every user, on every request, simultaneously.&lt;/p&gt;

&lt;p&gt;Suppose, for the sake of illustration, that this Data Access class holds some piece of state internally during a method call — even something as seemingly harmless as a private field tracking the employee ID currently being processed:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class clsWorkFlow : IclsWorkFlow&lt;br&gt;
{&lt;br&gt;
    private string _currentEmpId; // dangerous if this class is a Singleton&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public List&amp;lt;CtrlSlnoName&amp;gt; GetAddWorkFlows(int employeeSlno, int createdBy)
{
    _currentEmpId = employeeSlno.ToString();
    // ... fetch data using _currentEmpId ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;If two requests arrive close together — User A's request and User B's request — and both are being served by the same shared Singleton instance, this sequence becomes possible:&lt;/p&gt;

&lt;p&gt;User A's request begins, sets _currentEmpId = "1023"&lt;br&gt;
Before User A's request finishes processing, User B's request arrives and overwrites _currentEmpId = "2045", using the same shared instance&lt;br&gt;
User A's request completes, but by now _currentEmpId has been overwritten — User A's operation may complete using User B's data instead of their own&lt;/p&gt;

&lt;p&gt;No exception is thrown. No error is logged. The application appears to work correctly under light, sequential testing, and the failure only appears under real concurrent traffic — exactly the conditions that are hardest to reproduce in a typical development or QA environment.&lt;/p&gt;

&lt;p&gt;The Same Root Cause as a Familiar Bug&lt;/p&gt;

&lt;p&gt;This is structurally identical to a well-known ASP.NET problem: Session state being overwritten when a user has multiple browser tabs open. In that scenario, two tabs share the same Session object because they share the same session cookie; whichever tab writes last "wins," and the other tab's data is silently corrupted at the moment of save.&lt;/p&gt;

&lt;p&gt;The DI Singleton scenario is the same failure pattern at a different layer: instead of two browser tabs sharing one Session, it's two concurrent HTTP requests sharing one service instance. In both cases, the underlying mistake is the same — treating shared, mutable state as if it were private to a single operation, when in reality it's accessible to multiple operations happening at once.&lt;/p&gt;

&lt;p&gt;Why Scoped Is the Safer Default for Data Access&lt;/p&gt;

&lt;p&gt;Registering clsWorkFlow as Scoped instead of Singleton eliminates this entire class of problem for the common case:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
services.AddScoped();&lt;/p&gt;

&lt;p&gt;With Scoped lifetime, each HTTP request gets its own fresh instance of clsWorkFlow. User A's request and User B's request, even arriving at the exact same moment, are working with two completely separate objects. There's no shared internal field to overwrite, because there's no sharing happening at all between requests.&lt;/p&gt;

&lt;p&gt;This is why Scoped is the conventional default for Data Access classes and anything tied to per-request context (like a database connection or a current-user identifier): it matches the natural boundary of "one request, one unit of work" without the overhead of creating a brand-new instance multiple times within a single request, which is what Transient would do unnecessarily.&lt;/p&gt;

&lt;p&gt;Why This Isn't About Authorization&lt;/p&gt;

&lt;p&gt;It's worth being precise about what this problem is, and what it isn't. Role-based access control — checking whether a user is authorized to see or modify certain data — is a completely separate concern from thread-safety and instance scoping. A system can have perfectly correct authorization logic and still suffer from this exact bug, because the two problems operate at different layers: authorization determines what a user is allowed to access; scoping determines whether concurrent operations interfere with each other at the object level. Fixing one does nothing to address the other.&lt;/p&gt;

&lt;p&gt;A Practical Check&lt;/p&gt;

&lt;p&gt;A useful habit when registering any service in DependencyInjection.cs: ask whether the class holds any state that changes during a method call, even temporarily. If it does, and it's registered as Singleton, that state is a shared resource across every concurrent user — a strong candidate for the same category of bug described above. Stateless services, or services whose state is fully contained within a single method call and never stored as a field, are safe as Singletons. Anything that stores per-request or per-user data as instance state should almost always be Scoped instead.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;Dependency Injection lifetime selection isn't just a performance or memory optimization detail — choosing Singleton for a service that isn't genuinely safe to share across concurrent requests can silently reintroduce the same class of data-corruption bug found in classic Session-scoping mistakes, just at a different architectural layer. Recognizing the pattern — shared mutable state, accessed by more than one operation at once — makes it possible to spot this risk in code review before it ever reaches production, rather than debugging it after a support ticket reports data that doesn't make sense.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webapi</category>
      <category>dependencyinversion</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Controller and Data Access: Understanding 2-Layer Architecture Through Abstraction and Encapsulation</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Sat, 19 Sep 2026 04:04:07 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/controller-and-data-access-understanding-2-layer-architecture-through-abstraction-and-encapsulation-42ad</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/controller-and-data-access-understanding-2-layer-architecture-through-abstraction-and-encapsulation-42ad</guid>
      <description>&lt;p&gt;Not every real-world application needs full Clean Architecture with its multiple layers, dependency inversion, and strict domain boundaries. Many production systems — particularly internal enterprise applications like HRMS and Payroll platforms — run successfully on a simpler, pragmatic 2-layer architecture: a Controller layer and a Data Access layer. Understanding why this simpler structure works, and how it relates to the same OOP principles behind Clean Architecture, makes it easier to work confidently in either kind of codebase.&lt;/p&gt;

&lt;p&gt;The Two Layers&lt;/p&gt;

&lt;p&gt;A 2-layer architecture splits an application into exactly what the name suggests:&lt;/p&gt;

&lt;p&gt;Layer 1 — Controller Layer: handles incoming HTTP requests, and delegates the actual work elsewhere.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
[ApiController]&lt;br&gt;
[Route("api/[controller]")]&lt;br&gt;
public class LeaveRequestController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    private readonly ILeaveRequestDataAccess _dataAccess;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public LeaveRequestController(ILeaveRequestDataAccess dataAccess)
{
    _dataAccess = dataAccess;
}

[HttpGet("get-emp-details")]
public IActionResult GetEmployeeDetails(string empId)
{
    var result = _dataAccess.GetEmployeeDetails(empId);
    return Ok(result);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;Layer 2 — Data Access Layer: handles the actual database interaction — connection strings, parameters, stored procedure or package calls.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public interface ILeaveRequestDataAccess&lt;br&gt;
{&lt;br&gt;
    EmployeeDto GetEmployeeDetails(string empId);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;public class LeaveRequestDataAccess : ILeaveRequestDataAccess&lt;br&gt;
{&lt;br&gt;
    public EmployeeDto GetEmployeeDetails(string empId)&lt;br&gt;
    {&lt;br&gt;
        // ADO.NET/Oracle-specific logic lives here:&lt;br&gt;
        // connection string, OracleCommand, package name, parameter types&lt;br&gt;
        return employeeData;&lt;br&gt;
    }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;The Controller never touches ADO.NET directly. The Data Access layer never touches HTTP concerns like status codes or routing. Each layer has exactly one job.&lt;/p&gt;

&lt;p&gt;Why This Is Abstraction and Encapsulation in Practice&lt;/p&gt;

&lt;p&gt;This separation isn't just organizational tidiness — it's a direct, practical application of two core OOP principles.&lt;/p&gt;

&lt;p&gt;Abstraction: the Controller calls _dataAccess.GetEmployeeDetails(empId) without knowing or caring how that data actually gets retrieved. It doesn't know whether the underlying call uses a stored procedure, a package function, or raw SQL. It only needs to know that calling this method returns the data it asked for. The internal complexity of connection handling, parameter binding, and query execution is hidden behind a simple method signature.&lt;/p&gt;

&lt;p&gt;Encapsulation: the Data Access layer protects the details of how data is fetched from being scattered across the codebase. If every controller method directly opened its own OracleConnection and wrote its own SQL, any change to the database — a renamed package, a new parameter, a switched provider — would require hunting down and editing code in dozens of places. By encapsulating that logic inside one dedicated layer, a change only needs to happen once.&lt;/p&gt;

&lt;p&gt;The Practical Test: What Happens When the Database Changes&lt;/p&gt;

&lt;p&gt;A useful way to confirm this separation is working correctly is to ask: if the underlying database technology changed — say, from Oracle to SQL Server — which layer would need to change?&lt;/p&gt;

&lt;p&gt;The answer should be: only the Data Access layer. The connection strings, parameter types, and query syntax living inside LeaveRequestDataAccess would need rewriting. The Controller, which only knows about the ILeaveRequestDataAccess interface and calls GetEmployeeDetails(empId), wouldn't need to change at all — it has no idea, and no need to know, what database sits behind that interface.&lt;/p&gt;

&lt;p&gt;If a database change ever required touching Controller code too, that would be a signal the separation isn't clean — some data-access detail has leaked into a layer that shouldn't know about it.&lt;/p&gt;

&lt;p&gt;How This Relates to Clean Architecture&lt;/p&gt;

&lt;p&gt;Clean Architecture takes this same underlying idea — separating concerns so that change in one area doesn't ripple through the whole system — and extends it further, typically into more layers: a Domain layer (core business rules, independent of any framework), an Application layer (use cases, orchestration), an Infrastructure layer (database, external services), and a Presentation layer (API or UI).&lt;/p&gt;

&lt;p&gt;The core principle is identical to the 2-layer setup: outer layers depend on inner layers, never the other way around, and each layer only knows what it strictly needs to know. A 2-layer Controller/Data Access split is, in effect, a simplified version of this same idea — fewer layers, less ceremony, but the same underlying discipline of not letting database-specific details leak into request-handling code, and not letting request-handling concerns leak into data-access code.&lt;/p&gt;

&lt;p&gt;For many internal enterprise applications, a full Clean Architecture setup — with separate Domain, Application, and Infrastructure projects, dependency inversion containers, and strict boundary enforcement — is more structure than the project actually needs. A well-maintained 2-layer architecture, with a clear interface between Controller and Data Access, delivers much of the same practical benefit — testability, easier maintenance, isolated change — without the additional overhead.&lt;/p&gt;

&lt;p&gt;When 2 Layers Stop Being Enough&lt;/p&gt;

&lt;p&gt;The 2-layer pattern works well until business logic starts accumulating inside the Controller itself — validation rules, calculations, workflow decisions that have nothing to do with HTTP handling or database access. When that happens, it's usually a sign that a third layer — often called a Service or Business Logic layer — is needed to hold that logic separately, keeping the Controller thin and focused purely on request/response handling. This is often the natural next step toward something closer to Clean Architecture, added incrementally as a project's complexity genuinely justifies it, rather than adopted wholesale from day one.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;A 2-layer Controller/Data Access architecture isn't a simplified shortcut that skips "real" architecture — it's a direct, practical application of Abstraction and Encapsulation at the system level, just as those same principles apply within a single class. Understanding it this way makes the reasoning behind Clean Architecture's more elaborate layering easier to grasp too: both are solving the same underlying problem — isolating change, hiding implementation detail, and keeping each part of the system focused on exactly one responsibility — just at different levels of complexity depending on what the project actually needs.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>architecture</category>
      <category>csharp</category>
      <category>oop</category>
    </item>
    <item>
      <title>GET, POST, PUT, DELETE: Choosing HTTP Verbs in a Real .NET Core Web API, Not Just by the Textbook</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Fri, 18 Sep 2026 11:02:43 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/get-post-put-delete-choosing-http-verbs-in-a-real-net-core-web-api-not-just-by-the-textbook-2h8</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/get-post-put-delete-choosing-http-verbs-in-a-real-net-core-web-api-not-just-by-the-textbook-2h8</guid>
      <description>&lt;p&gt;Most explanations of HTTP verbs in ASP.NET Core Web API stop at the dictionary definition: GET reads, POST creates, PUT updates, DELETE removes. That's true, but it doesn't explain why real enterprise APIs often bend these rules — and understanding why matters more in practice than reciting the definitions. This article walks through HTTP verb selection using a real HR/Payroll workflow scenario: submitting, updating, and removing leave requests.&lt;/p&gt;

&lt;p&gt;The Textbook Mapping&lt;/p&gt;

&lt;p&gt;In a typical Web API controller, the four core verbs map to CRUD operations like this:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
[ApiController]&lt;br&gt;
[Route("api/[controller]")]&lt;br&gt;
public class LeaveRequestController : ControllerBase&lt;br&gt;
{&lt;br&gt;
    [HttpGet("get-emp-details")]&lt;br&gt;
    public IActionResult GetEmployeeDetails(int finSlno, string empId, int createdBy)&lt;br&gt;
    {&lt;br&gt;
        // fetch and return data&lt;br&gt;
        return Ok(employeeDetails);&lt;br&gt;
    }&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[HttpPost("save-leave-request")]
public IActionResult SaveLeaveRequest([FromBody] LeaveRequestDto request)
{
    // create a new record
    return Ok(result);
}

[HttpPut("update-leave-request")]
public IActionResult UpdateLeaveRequest([FromBody] LeaveRequestDto request)
{
    // update an existing record
    return Ok(result);
}

[HttpDelete("delete-leave-request/{id}")]
public IActionResult DeleteLeaveRequest(int id)
{
    // remove a record
    return Ok(result);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}&lt;/p&gt;

&lt;p&gt;This is correct, and it's what most tutorials teach. But real enterprise systems — particularly HRMS and Payroll applications — frequently deviate from this pattern for reasons that only make sense once you understand what's actually happening to the data underneath.&lt;/p&gt;

&lt;p&gt;Why "Delete" Often Isn't Really DELETE&lt;/p&gt;

&lt;p&gt;Consider a leave request that an employee wants to withdraw. The textbook answer says: call [HttpDelete], remove the row from the database, done.&lt;/p&gt;

&lt;p&gt;In practice, most HR and Payroll systems don't want a "delete" action to genuinely erase the record. There are real, practical reasons for this:&lt;/p&gt;

&lt;p&gt;Audit trails — organizations often need to prove what happened and when, especially for anything touching attendance, payroll, or approvals. Permanently deleting a leave request destroys that history.&lt;br&gt;
Compliance — payroll systems in particular are subject to regulations that may require retaining records for a defined period, regardless of whether the user considers them "deleted."&lt;br&gt;
Traceability during disputes — if an employee claims a leave request was withdrawn incorrectly, having the original record (even if marked inactive) is the only way to investigate what actually happened.&lt;/p&gt;

&lt;p&gt;Because of this, many systems implement what's called a soft delete: instead of removing the row, the operation updates a status field:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
public class LeaveRequest&lt;br&gt;
{&lt;br&gt;
    public int LeaveRequestId;&lt;br&gt;
    public string EmpID;&lt;br&gt;
    public int Status; // 0 = Pending, 1 = Approved, 2 = Withdrawn&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;A "delete" action becomes, in reality, an update to Status:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
[HttpPost("save-delete-role-mapping")]&lt;br&gt;
public IActionResult SoftDeleteLeaveRequest([FromBody] List requests)&lt;br&gt;
{&lt;br&gt;
    foreach (var req in requests)&lt;br&gt;
    {&lt;br&gt;
        req.Status = 2; // mark as withdrawn, don't remove the row&lt;br&gt;
    }&lt;br&gt;
    // save changes&lt;br&gt;
    return Ok(result);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Because this is technically an update operation, not a true deletion, using &lt;a href="https://dev.toor%20[HttpPut]"&gt;HttpPost&lt;/a&gt; instead of [HttpDelete] is a defensible, common real-world choice — not a mistake or a shortcut. The verb should reflect what actually happens to the data, not just what the button on the screen says.&lt;/p&gt;

&lt;p&gt;Why POST Sometimes Wins Even for Genuine Deletes&lt;/p&gt;

&lt;p&gt;There's a second, more practical reason POST shows up in place of DELETE in real systems: payload complexity.&lt;/p&gt;

&lt;p&gt;[HttpDelete] is traditionally designed around simple identifiers, often passed in the URL:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
[HttpDelete("delete-leave-request/{id}")]&lt;br&gt;
public IActionResult Delete(int id) { ... }&lt;/p&gt;

&lt;p&gt;This works cleanly for deleting one record by one ID. But real operations are frequently batch operations — deleting or withdrawing multiple leave requests at once, each potentially needing additional context (who's performing the action, why, which workflow step it affects):&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
[HttpPost("save-delete-role-mapping")]&lt;br&gt;
public IActionResult BatchDelete([FromBody] List requests) { ... }&lt;/p&gt;

&lt;p&gt;Passing a list of complex objects in a DELETE request body is technically possible but goes against how DELETE has traditionally been used and supported across tooling. POST, which has always supported rich request bodies, handles this case more naturally — which is exactly why it's common to see "delete" operations implemented as POST endpoints in real enterprise codebases.&lt;/p&gt;

&lt;p&gt;A Practical Rule of Thumb&lt;/p&gt;

&lt;p&gt;None of this means HTTP verb conventions don't matter — they're still a useful default. The practical guidance is:&lt;/p&gt;

&lt;p&gt;Use GET for anything that only reads data and has no side effects&lt;br&gt;
Use POST for creating new records, and also for operations — including some deletes — that involve complex payloads or don't map cleanly to a single identifier&lt;br&gt;
Use PUT for straightforward updates to an existing, fully-identified record&lt;br&gt;
Use DELETE for simple, single-identifier removals where a true, permanent delete is actually intended&lt;/p&gt;

&lt;p&gt;When you encounter an API that doesn't follow the textbook mapping exactly — like a "delete" endpoint implemented as POST — it's worth asking why, rather than assuming it's wrong. Often, as in the soft-delete case above, there's a real business reason: the data isn't actually being removed, it's being marked, and the verb reflects that reality rather than the UI label.&lt;/p&gt;

&lt;p&gt;Takeaway&lt;/p&gt;

&lt;p&gt;HTTP verb selection in a real Web API isn't just about matching a CRUD acronym to a decorator. It's about accurately representing what actually happens to the underlying data. A "delete" that's really a status update should probably be a POST or PUT, not a DELETE — and recognizing that distinction is a sign of understanding the system, not a shortcut around convention.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>dotnetcore</category>
      <category>webapi</category>
      <category>backend</category>
    </item>
    <item>
      <title>OOP Concepts Explained Through a Real Payroll System, Not Animals and Shapes</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Thu, 17 Sep 2026 10:16:06 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/oop-concepts-explained-through-a-real-payroll-system-not-animals-and-shapes-jli</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/oop-concepts-explained-through-a-real-payroll-system-not-animals-and-shapes-jli</guid>
      <description>&lt;p&gt;Most OOP tutorials teach Class, Object, Abstraction, Encapsulation, Inheritance, and Polymorphism using Animal/Dog or Shape/Circle examples. They're fine for a first pass, but they rarely translate to how these concepts actually show up in real enterprise applications. This article walks through all four OOP pillars using a real, common enterprise scenario: a Payroll/HRMS leave-request and multi-level approval workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Class vs Object: The Blueprint vs the Real Record
&lt;/h2&gt;

&lt;p&gt;In a Payroll/HRMS system, an "Employee" or a "Leave Request" isn't just an abstract idea — it's a real record with real fields, submitted by real people.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;Class&lt;/strong&gt; is the blueprint — the definition of what fields and behavior every Leave Request will have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaveRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;EmpID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;LeaveRequestID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;FromDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;ToDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;strong&gt;Object&lt;/strong&gt; is one actual instance of that blueprint, filled with real data — a specific employee's specific leave request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;LeaveRequest&lt;/span&gt; &lt;span class="n"&gt;req1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LeaveRequest&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt; 
    &lt;span class="n"&gt;EmpID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1023"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;FromDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
    &lt;span class="n"&gt;ToDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
    &lt;span class="n"&gt;Reason&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Personal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; 
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Class exists once, in code. Objects exist many times — one for every leave request submitted across the organization, each with its own real values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abstraction: Hiding the Machinery Behind "Submit"
&lt;/h2&gt;

&lt;p&gt;When an employee clicks "Submit" on a leave request, they don't see — and don't need to see — everything happening behind that click: parameter type validation, opening the database connection, executing the stored procedure, committing the transaction if everything succeeds, rolling back if something fails, and handling any errors along the way.&lt;/p&gt;

&lt;p&gt;All the employee sees is a success message. That's Abstraction: exposing only what's necessary to the caller, while hiding the internal complexity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;SubmitLeaveRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LeaveRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Hidden internally: connection handling, parameter checks,&lt;/span&gt;
    &lt;span class="c1"&gt;// commit/rollback logic, error handling&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// caller just sees success or failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The calling code — whether it's a UI button or another service — doesn't need to know &lt;em&gt;how&lt;/em&gt; the request gets saved, only &lt;em&gt;that&lt;/em&gt; it did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encapsulation: Protecting Status From Being Changed Carelessly
&lt;/h2&gt;

&lt;p&gt;A Leave Request's &lt;code&gt;Status&lt;/code&gt; field shouldn't be something any part of the application can set directly. In a real approval workflow, a request typically moves through multiple levels — each level compares a workflow sequence number (&lt;code&gt;wfSeqSlNo&lt;/code&gt;) against the next expected value (&lt;code&gt;nextWfSeqSlNo&lt;/code&gt;); only when they match does the request actually progress toward "Approved."&lt;/p&gt;

&lt;p&gt;If any code could bypass that check and set &lt;code&gt;Status&lt;/code&gt; directly, a request could end up marked "Approved" without actually completing all required approval levels — a real compliance and payroll accuracy risk, not just a coding inconvenience.&lt;/p&gt;

&lt;p&gt;Encapsulation protects against this by restricting direct access and only allowing changes through controlled logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaveRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApproveLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentWfSeqSlNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nextWfSeqSlNo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentWfSeqSlNo&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nextWfSeqSlNo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, &lt;code&gt;Status&lt;/code&gt; can only change through &lt;code&gt;ApproveLevel()&lt;/code&gt;, which enforces the actual business rule — no code path can quietly skip the approval chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inheritance: Sharing Common Fields Across Request Types
&lt;/h2&gt;

&lt;p&gt;A Payroll/HRMS system rarely has just one kind of request. Alongside Leave Requests, there are often Advance Requests, Reimbursement Requests, and others — and most of them share the same underlying fields: an employee ID, a status, a submission date, and the same multi-level approval logic.&lt;/p&gt;

&lt;p&gt;Rather than duplicating that logic in every request class, Inheritance lets you define it once, in a shared base class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;EmpID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;SubmittedDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ApproveLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;currentWfSeqSlNo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nextWfSeqSlNo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentWfSeqSlNo&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nextWfSeqSlNo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaveRequest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;FromDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;ToDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdvanceRequest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;AdvanceAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Purpose&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both &lt;code&gt;LeaveRequest&lt;/code&gt; and &lt;code&gt;AdvanceRequest&lt;/code&gt; automatically get &lt;code&gt;EmpID&lt;/code&gt;, &lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;SubmittedDate&lt;/code&gt;, and the approval logic — without rewriting it. Each class only needs to define what's actually different about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Polymorphism: Each Request Type Handling Its Own Notification
&lt;/h2&gt;

&lt;p&gt;Once a request is approved, the notification message should differ by type — a Leave approval message looks different from an Advance approval message. Polymorphism allows each child class to override a shared method with its own specific behavior, while still being treated as the same general type elsewhere in the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;virtual&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetApprovalMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Your request has been approved."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LeaveRequest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;FromDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;ToDate&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetApprovalMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"Your leave from &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;FromDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; to &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ToDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is approved."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AdvanceRequest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;AdvanceAmount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetApprovalMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"Your advance of &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AdvanceAmount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is approved and will reflect in next payroll."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical benefit shows up when handling a mixed collection of requests — a list containing both Leave and Advance requests, for example. Calling &lt;code&gt;GetApprovalMessage()&lt;/code&gt; on each one automatically returns the correct message for its actual type, without writing manual &lt;code&gt;if/else&lt;/code&gt; checks to figure out what kind of request it is first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interface vs Abstract Class: Two Ways to Enforce a Contract
&lt;/h2&gt;

&lt;p&gt;Once you have a shared &lt;code&gt;BaseRequest&lt;/code&gt; class, a natural question comes up: should every request type be forced to have its own &lt;code&gt;GetApprovalMessage()&lt;/code&gt;, and if so, how strict should that requirement be?&lt;/p&gt;

&lt;p&gt;&lt;code&gt;BaseRequest&lt;/code&gt; above is an &lt;strong&gt;abstract class&lt;/strong&gt; — it already provides real, working code (&lt;code&gt;EmpID&lt;/code&gt;, &lt;code&gt;Status&lt;/code&gt;, &lt;code&gt;ApproveLevel()&lt;/code&gt;), while still requiring child classes to implement their own version of specific methods. It gives you something &lt;em&gt;and&lt;/em&gt; asks for something in return.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;interface&lt;/strong&gt; goes further: it provides &lt;em&gt;zero&lt;/em&gt; implementation, only a contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IApprovalNotifiable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetApprovalMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any class implementing &lt;code&gt;IApprovalNotifiable&lt;/code&gt; is guaranteed to have a &lt;code&gt;GetApprovalMessage()&lt;/code&gt; method — but gets no code for free. Compare that to &lt;code&gt;BaseRequest&lt;/code&gt;, which hands &lt;code&gt;LeaveRequest&lt;/code&gt; and &lt;code&gt;AdvanceRequest&lt;/code&gt; real, working fields and methods before they write a single line themselves.&lt;/p&gt;

&lt;p&gt;The practical distinction: use an &lt;strong&gt;abstract class&lt;/strong&gt; when related types share meaningful common code worth writing once (like the approval logic every request type needs identically). Use an &lt;strong&gt;interface&lt;/strong&gt; when you only need to guarantee that a capability exists — regardless of how differently each class chooses to implement it — with no shared code involved at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Framing Helps
&lt;/h2&gt;

&lt;p&gt;Generic Animal/Shape examples explain the mechanics of OOP well enough, but they don't show &lt;em&gt;why&lt;/em&gt; any of it matters in a real system. Framed around an actual enterprise workflow — with real fields like &lt;code&gt;EmpID&lt;/code&gt;, &lt;code&gt;wfSeqSlNo&lt;/code&gt;, and real business risk behind getting &lt;code&gt;Status&lt;/code&gt; wrong — these same five concepts stop being abstract syntax rules and start looking like decisions with real consequences: fewer duplicated classes, safer status transitions, and cleaner notification logic across multiple request types.&lt;/p&gt;

&lt;p&gt;If you're working on any enterprise system with multi-step approval workflows — payroll, HR, procurement, or similar — these same patterns are very likely already present in your codebase, whether or not they've been named explicitly as Class, Abstraction, Encapsulation, Inheritance, and Polymorphism.&lt;/p&gt;

</description>
      <category>oop</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>dotnetfundamentals</category>
    </item>
    <item>
      <title>Debugging ORA-06550 / PLS-00201 in ADO.NET: A Checklist Before You Start Changing Permissions</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Mon, 07 Sep 2026 17:54:45 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/debugging-ora-06550-pls-00201-in-adonet-a-checklist-before-you-start-changing-permissions-30pb</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/debugging-ora-06550-pls-00201-in-adonet-a-checklist-before-you-start-changing-permissions-30pb</guid>
      <description>&lt;p&gt;Enterprise applications rarely live in a single, tidy Oracle schema. It's common to have one schema owning HR-related objects and another owning a different module, with application code that needs to call across both — and that's exactly where a specific, easy-to-misdiagnose error shows up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plsql"&gt;&lt;code&gt;&lt;span class="n"&gt;ORA&lt;/span&gt;&lt;span class="mi"&gt;-06550&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;PLS&lt;/span&gt;&lt;span class="mi"&gt;-00201&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;identifier&lt;/span&gt; &lt;span class="o"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SCHEMA_B.PKG_SOME_PACKAGE&lt;/span&gt;&lt;span class="o"&gt;'&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt;
&lt;span class="n"&gt;ORA&lt;/span&gt;&lt;span class="mi"&gt;-06550&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;column&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="n"&gt;PL&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;Statement&lt;/span&gt; &lt;span class="n"&gt;ignored&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The instinctive reaction is to jump straight to &lt;code&gt;GRANT EXECUTE&lt;/code&gt; and hope it fixes things. Sometimes that is the fix — but this error can come from several different root causes that look identical from the outside, so the more reliable approach is to verify systematically before changing anything on the database side.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;An ADO.NET connection authenticates as one schema (call it Schema A). The code needs to call a package that physically lives in a different schema (Schema B):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OracleCommand&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OracleCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SCHEMA_B.PKG_SOME_PACKAGE.GET_DETAILS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CommandType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StoredProcedure&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// parameters, execute, etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On execution, Oracle throws &lt;code&gt;PLS-00201: identifier ... must be declared&lt;/code&gt; — even though the package genuinely exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Jumping Straight to Grants Is a Mistake
&lt;/h2&gt;

&lt;p&gt;This error message doesn't distinguish between several different possible causes, and blindly granting permissions wastes time when the real issue is something else entirely. Before touching grants, work through these checks in order:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Confirm the exact object name and owning schema.&lt;/strong&gt;&lt;br&gt;
Copy-pasted code between similar procedures is a common source of subtle mismatches — a missing schema prefix, a package name that's slightly off, or an object that was renamed since the code was last touched. Query directly to confirm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;object_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;all_objects&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'SCHEMA_B'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;object_name&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%SOME_PACKAGE%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't rely on memory or an old script for the exact name — verify it fresh, every time this error appears.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Check the object's status, not just its existence.&lt;/strong&gt;&lt;br&gt;
An object can technically exist but be in an &lt;code&gt;INVALID&lt;/code&gt; state (often after a dependent object changes). The query above includes &lt;code&gt;status&lt;/code&gt; for exactly this reason — an invalid package can throw errors that look like an access problem but are actually a compilation problem upstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verify the parameters being passed match the procedure's actual signature.&lt;/strong&gt;&lt;br&gt;
A mismatch in parameter count, order, or data type can sometimes surface as a resolution error rather than a clear "wrong parameters" message, especially with overloaded procedures inside a package. Double-check the package specification for the exact parameter list and types before assuming the problem is schema-related at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Only now, check execute privileges.&lt;/strong&gt;&lt;br&gt;
If the name is confirmed correct, the object is valid, and parameters match — then it's genuinely a privilege issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;EXECUTE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;PKG_SOME_PACKAGE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;SCHEMA_A_USER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this grant, the calling schema's session cannot resolve the package, regardless of how correctly the call is written.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Consider a synonym for cleaner long-term calls.&lt;/strong&gt;&lt;br&gt;
A grant alone lets the call work with a fully-qualified name, but a synonym in the calling schema avoids hardcoding the cross-schema prefix everywhere in application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;SYNONYM&lt;/span&gt; &lt;span class="n"&gt;PKG_SOME_PACKAGE&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="n"&gt;SCHEMA_B&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PKG_SOME_PACKAGE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also makes future changes easier — if the object's location ever changes, only the synonym needs updating, not every call site in the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Combination Trips Up Developers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The error message doesn't tell you &lt;em&gt;which&lt;/em&gt; of several possible causes you're dealing with, so it's tempting to guess rather than verify&lt;/li&gt;
&lt;li&gt;Multi-schema Oracle setups are common in enterprise systems, but many developers' daily experience is single-schema, making this class of error unfamiliar&lt;/li&gt;
&lt;li&gt;Under time pressure, it's easy to skip straight to "just grant it" without confirming the actual object, its status, or the parameters being sent — sometimes fixing the wrong thing while the real cause goes unnoticed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;When you hit &lt;code&gt;PLS-00201&lt;/code&gt; across schemas in an ADO.NET + Oracle setup, resist the urge to immediately change permissions. Work through it as a checklist: confirm the exact object name and schema, check that the object is valid (not just present), verify the parameters match the procedure's actual signature, and only then move to grants and synonyms. This order matters — it's faster overall than guessing, and it avoids the situation where a grant gets added that was never actually the problem, while the real cause stays hidden.&lt;/p&gt;

&lt;p&gt;In multi-schema enterprise applications, this kind of methodical, don't-skip-steps debugging saves real time compared to reactive permission changes.&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>The ASP.NET Session Mistake That Broke Our Multi-Tab Workflow</title>
      <dc:creator>Dhana</dc:creator>
      <pubDate>Mon, 07 Sep 2026 16:31:10 +0000</pubDate>
      <link>https://dev.to/dhanagani_lakshmi_2487ad0/the-aspnet-session-mistake-that-broke-our-multi-tab-workflow-jka</link>
      <guid>https://dev.to/dhanagani_lakshmi_2487ad0/the-aspnet-session-mistake-that-broke-our-multi-tab-workflow-jka</guid>
      <description>&lt;p&gt;If you've worked on any mid-sized ASP.NET application that uses &lt;code&gt;Session&lt;/code&gt; to hold user or transaction context, you've probably run into a bug that looks &lt;em&gt;impossible&lt;/em&gt; to reproduce — until you realize the user had two browser tabs open.&lt;/p&gt;

&lt;p&gt;Here's the exact scenario I ran into, and why it's more dangerous than it first looks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A user logs in with one ID. That's one &lt;code&gt;Session&lt;/code&gt; on the server, tied to that one &lt;code&gt;ASP.NET_SessionId&lt;/code&gt; cookie.&lt;/p&gt;

&lt;p&gt;Now the user opens two tabs of the same app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tab 1&lt;/strong&gt;: opens Employee A's record — who is eligible for Transaction Type 1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tab 2&lt;/strong&gt;: opens Employee B's record — who is eligible for Transaction Type 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both tabs are using the &lt;em&gt;same browser session cookie&lt;/em&gt;, which means both tabs are reading and writing to the &lt;strong&gt;exact same &lt;code&gt;Session&lt;/code&gt; object&lt;/strong&gt; on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Breaks
&lt;/h2&gt;

&lt;p&gt;Say the app stores the currently selected employee and their eligibility flag in Session:&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"CurrentEmployeeId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;employeeId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"EligibleTransactionType"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eligibilityType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user is working in Tab 1 (Employee A), then switches to Tab 2 and opens Employee B. That action &lt;strong&gt;overwrites&lt;/strong&gt; &lt;code&gt;Session["CurrentEmployeeId"]&lt;/code&gt; and &lt;code&gt;Session["EligibleTransactionType"]&lt;/code&gt; with Employee B's values.&lt;/p&gt;

&lt;p&gt;Now the user switches back to Tab 1 and clicks &lt;strong&gt;Save&lt;/strong&gt;. The save action reads eligibility from &lt;code&gt;Session["EligibleTransactionType"]&lt;/code&gt; — but that value now belongs to Employee B, not Employee A. If there's no server-side re-validation at the point of save, &lt;strong&gt;Employee A's transaction gets saved using Employee B's eligibility rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No exception is thrown. No error is logged. The data just quietly becomes wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Worse Than a Normal Bug
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's &lt;strong&gt;intermittent&lt;/strong&gt; — only happens with multiple tabs, so it's hard to reproduce in testing&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;passes all standard QA&lt;/strong&gt; — single-tab test cases work fine&lt;/li&gt;
&lt;li&gt;It causes &lt;strong&gt;silent data corruption&lt;/strong&gt;, not a crash — which means it can sit undetected in production for a long time&lt;/li&gt;
&lt;li&gt;It often gets misdiagnosed as a "random" data issue rather than a session-scoping bug&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;The core misunderstanding is this: &lt;strong&gt;&lt;code&gt;Session&lt;/code&gt; in ASP.NET is scoped to the user's session cookie, not to a browser tab.&lt;/strong&gt; All tabs in the same browser share the same session cookie by default, so they share the exact same &lt;code&gt;Session&lt;/code&gt; object on the server. There is no built-in per-tab isolation.&lt;/p&gt;

&lt;p&gt;Junior devs often mentally model &lt;code&gt;Session&lt;/code&gt; as "this user's current screen state," when it's really "this user's shared server-side bucket, accessible from anywhere they have a tab open."&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Actually Fix It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Never trust Session state alone for validation at save time.&lt;/strong&gt;&lt;br&gt;
Re-fetch and re-validate eligibility from the database (or source of truth) at the moment of save, using the entity ID that's actually being saved — not whatever happens to be sitting in Session.&lt;/p&gt;

&lt;p&gt;csharp&lt;br&gt;
 &lt;strong&gt;Bad&lt;/strong&gt;: trusts session blindly&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"EligibleTransactionType"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;requestedType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Better&lt;/strong&gt;: re-validate against the actual record being saved&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentEligibility&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_employeeService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEligibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;employeeIdFromForm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentEligibility&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;requestedType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Pass context explicitly through the request, not implicitly through Session.&lt;/strong&gt;&lt;br&gt;
Instead of relying on &lt;code&gt;Session["CurrentEmployeeId"]&lt;/code&gt;, pass the employee ID as a hidden field, route parameter, or query string on each screen. Each tab then carries its own context in the request itself, not in a shared server bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. If you truly need tab-level isolation, use TempData or per-request tokens carefully — not Session.&lt;/strong&gt;&lt;br&gt;
Session is inherently shared across tabs; if isolated state per tab is a real requirement, look at approaches like encoding context in the URL, using unique form tokens per screen instance, or client-side state management instead of leaning on &lt;code&gt;Session&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Add server-side authorization/business rule checks as a final gate before any write.&lt;/strong&gt;&lt;br&gt;
Treat every save action as if the client could send anything — because in a multi-tab scenario, it effectively can. The last line of defense should always be a fresh check against the source of truth, not a value that was set several clicks ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Session&lt;/code&gt; feels like it belongs to "this screen," but it actually belongs to "this user, everywhere they're logged in right now." Any app doing multi-step or multi-screen validation needs to treat Session as a convenience cache, never as the final source of truth at the point of a write. The fix isn't complicated — re-validate on save — but the bug is easy to miss precisely because it only shows up when real users behave in ways your test cases didn't cover.&lt;/p&gt;

&lt;p&gt;If your app has any workflow where a user might reasonably have two screens open at once, it's worth auditing every &lt;code&gt;Session&lt;/code&gt;-dependent save action today, before it becomes a production incident.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnet</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
