LocalDB cannot open database requested by login. Login failed.

Today I was working with a database using LocalDB server and I had set this specific database named CBA as the default one for my user LENIEL-PC\Leniel. Setting a default database for the user is useful because when you login using SQL Server Management Studio [ SSMS ] you go directly to that database, that is, when running queries that’s the database used and not the master one. This is life saver.

I wanted to restore a backup file .bak I got from the production server using the Restore Database… command in SSMS. This is to test locally but first I had to drop my local database copy. I did drop my local copy while I was logged in to that database. I Selected the database in Object Explorer, right clicked it and selected Delete. Then checked the Close existing connections (bottom of the window), clicked OK and then the deletion succeeded. Pretty good… no.

What happened? The next time I tried to login again in SSMS this was what I got:

SSMS | Cannot connect to (localdb)\v11.0.Figure 1 - SSMS | Cannot connect to (localdb)\v11.0.
Cannot open database requested by login. The login failed.
Login failed for user LENIEL-PC\Leniel (Microsoft SQL Server, Error: 4060)

Pretty obvious! I deleted the default database I had set to my user LENIEL-PC\Leniel. Now what? How can I login to the database server and change the default database? Good question… Smile

This “simple” error was such a pain to fix…I tried lots of things like using SqlLocalDB.exe utility to stop and delete that specific LocalDB instance named v11.0. I would create it again afterwards if deleting it worked at least. This was the message logged in Windows Event logs when I tried to delete the instance:

Windows API call LogSqlDiagRec returned error code: 0. Windows system error message is: The operation completed successfully.
Reported at line: 3791. [Microsoft][ODBC SQL Server Driver][SQL Server]Cannot open database "CBA" requested by the login. The login failed.

As you see the problem was the same. It was barking about the deleted database. Damn it!

I also tried the login with the sa account but guess what… the sa account was disabled for that LocalDB instance. Even if it was enabled I would not be able to login because I didn’t know the password for sa.

Googling more I managed to find this post by Pinal Dave:

SQL SERVER – Fix : Error: 4064 – Cannot open user default database. Login failed. Login failed for user

So guess what: the solution is ridiculously easy once you know what to do of course…

Click that Options >> button in Figure 1. Now select the Connection Properties tab.

SSMS Connect to Server | Connection Properties | Connect to database optionFigure 2 - SSMS Connect to Server | Connection Properties | Connect to database option

I had to type master in Connect to database field since I did not have it in the list of available databases.

Now click connect and you’re done.

Great. Thanks God I was able to login and access the database server.

What’s left?

Reset my user Default database to master so that I can login again directly. By the way, the field Default database was empty since I had dropped the database!

Doing so there’s no need to set the Connect to database option anymore.

SSMS | Setting Default database for userFigure 3 - SSMS | Setting Default database for user

Sometimes things are so easy but they are hidden somewhere we just can’t figure it out at first.

Hope it helps.

NPOI 2.0 - Excel 2007 [ XLSX ] and Word 2007 [ DOCX ] support

This is the 2nd post of a series of posts about NPOI 2.0.

NPOI is jumping from version 1.2.5 directly to 2.0. A big reason for this is that it now supports Office 2007 formats (.xlsx and .docx). Given this fact, using NPOI 2.0 will seem a little bit more complicated than the previous version because NPOI now provides multiple namespaces, including HSSF (Excel 2003), XSSF (Excel 2007) and XWPF (Word 2007).

Don’t freak out! Confused smile We’re here to help you understand how to work with all those namespaces.

Excel 2007 support
Here’s a simple sample for Excel 2007…

using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;
using System.IO;

namespace NPOI.Examples.XSSF.SetCellValuesInXlsx
{
    class Program
    {
        static void Main(string[] args)
        {
            IWorkbook workbook = new XSSFWorkbook();
            
            ISheet sheet1 = workbook.CreateSheet("Sheet1");

            sheet1.CreateRow(0).CreateCell(0).SetCellValue("This is a Sample");
            
            int x = 1;
            
            for (int i = 1; i <= 15; i++)
            {
                IRow row = sheet1.CreateRow(i);
                
                for (int j = 0; j < 15; j++)
                {
                    row.CreateCell(j).SetCellValue(x++);
                }
            }

            FileStream sw = File.Create("test.xlsx");

            workbook.Write(sw);
            
            sw.Close();
        }
    }
}

To go deeper and explore all the features available for Excel 2007 you can check a handful of sample projects here (as of now 24 sample projects showcasing many available features):

https://github.com/tonyqus/npoi/tree/master/examples/xssf

Open the file NPOI.XSSF.Examples.2010.sln to have all them show up in Visual Studio’s Solution Explorer.

 Excel 2007 (NPOI.XSSF) sample projects listFigure 1 - Excel 2007 (NPOI.XSSF) sample projects list

Word 2007 support
Here’s a simple document example for Word 2007…

using NPOI.XWPF.UserModel;
using System.IO;

namespace SimpleDocument
{
    class Program
    {
        static void Main(string[] args)
        {
            XWPFDocument doc = new XWPFDocument();

            XWPFParagraph p1 = doc.CreateParagraph();
            p1.SetAlignment(ParagraphAlignment.CENTER);
            p1.SetBorderBottom(Borders.DOUBLE);
            p1.SetBorderTop(Borders.DOUBLE);

            p1.SetBorderRight(Borders.DOUBLE);
            p1.SetBorderLeft(Borders.DOUBLE);
            p1.SetBorderBetween(Borders.SINGLE);

            p1.SetVerticalAlignment(TextAlignment.TOP);

            XWPFRun r1 = p1.CreateRun();
            r1.SetBold(true);
            r1.SetText("The quick brown fox");
            r1.SetBold(true);
            r1.SetFontFamily("Courier");
            r1.SetUnderline(UnderlinePatterns.DotDotDash);
            r1.SetTextPosition(100);

            XWPFParagraph p2 = doc.CreateParagraph();
            p2.SetAlignment(ParagraphAlignment.RIGHT);

            //BORDERS
            p2.SetBorderBottom(Borders.DOUBLE);
            p2.SetBorderTop(Borders.DOUBLE);
            p2.SetBorderRight(Borders.DOUBLE);
            p2.SetBorderLeft(Borders.DOUBLE);
            p2.SetBorderBetween(Borders.SINGLE);

            XWPFRun r2 = p2.CreateRun();
            r2.SetText("jumped over the lazy dog");
            r2.SetStrike(true);
            r2.SetFontSize(20);

            XWPFRun r3 = p2.CreateRun();
            r3.SetText("and went away");
            r3.SetStrike(true);
            r3.SetFontSize(20);
            r3.SetSubscript(VerticalAlign.SUPERSCRIPT);


            XWPFParagraph p3 = doc.CreateParagraph();
            p3.SetWordWrap(true);
            p3.SetPageBreak(true);

            //p3.SetAlignment(ParagraphAlignment.DISTRIBUTE);
            p3.SetAlignment(ParagraphAlignment.BOTH);
            p3.SetSpacingLineRule(LineSpacingRule.EXACT);

            p3.SetSetIndentationFirstLine(600);


            XWPFRun r4 = p3.CreateRun();
            r4.SetTextPosition(20);
            r4.SetText("To be, or not to be: that is the question: "
                    + "Whether 'tis nobler in the mind to suffer "
                    + "The slings and arrows of outrageous fortune, "
                    + "Or to take arms against a sea of troubles, "
                    + "And by opposing end them? To die: to sleep; ");
            r4.AddBreak(BreakType.PAGE);
            r4.SetText("No more; and by a sleep to say we end "
                    + "The heart-ache and the thousand natural shocks "
                    + "That flesh is heir to, 'tis a consummation "
                    + "Devoutly to be wish'd. To die, to sleep; "
                    + "To sleep: perchance to dream: ay, there's the rub; "
                    + ".......");
            r4.SetItalic(true);
            //This would imply that this break shall be treated as a simple line break, and break the line after that word:

            XWPFRun r5 = p3.CreateRun();
            r5.SetTextPosition(-10);
            r5.SetText("For in that sleep of death what dreams may come");
            r5.AddCarriageReturn();
            r5.SetText("When we have shuffled off this mortal coil,"
                    + "Must give us pause: there's the respect"
                    + "That makes calamity of so long life;");
            r5.AddBreak();
            r5.SetText("For who would bear the whips and scorns of time,"
                    + "The oppressor's wrong, the proud man's contumely,");

            r5.AddBreak(BreakClear.ALL);
            r5.SetText("The pangs of despised love, the law's delay,"
                    + "The insolence of office and the spurns" + ".......");

            FileStream out1 = new FileStream("simple.docx", FileMode.Create);
            doc.Write(out1);
            out1.Close();
        }
    }
}

To go deeper an explore all features available for Word 2007 you can check a handful of sample projects here (as of now 5 sample projects showcasing some available features):

https://github.com/tonyqus/npoi/tree/master/examples/xwpf

Open the file NPOI.XWPF.Examples.2010.sln to have all them show up in Visual Studio’s Solution Explorer.

Word 2007 (NPOI.XWPF) sample projects listFigure 2 - Word 2007 (NPOI.XWPF) sample projects list

NPOI 2.0 - Automatic Format/Type Inference for Excel 2003/2007

This is the 1st post of a series of posts about NPOI 2.0.

NPOI is jumping from version 1.2.5 directly to 2.0. A big reason for this is that it now supports Office 2007 formats (.xlsx and .docx). Given this fact, using NPOI 2.0 will seem a little bit more complicated than the previous version because NPOI now provides multiple namespaces, including HSSF (Excel 2003), XSSF (Excel 2007) and XWPF (Word 2007).

To be able to automatically identify Excel formats when reading a file from the file system and to avoid you having to infer the file type yourself, NPOI provides a very convenient class NPOI.SS.WorkbookFactory.

public class WorkbookFactory
{
    public static WorkbookFactory Create(POIFSFileSystem fs){ ... }
    public static WorkbookFactory Create(OPCPackage pkg){ ... }
    public static IWorkbook Create(Stream inputStream){ ... }
    public static IFormulaEvaluator CreateFormulaEvaluator(IWorkbook workbook){ ... }
}

The difference between the first two Create methods introduced under POIFSFileSystem and OPCPackage is that POIFSFileSystem reader library uses OLE2 format and OPCPackage uses OOXML format (commonly known as ActiveX Document Format). These two libraries are used to read both Excel 2003 (.xls) and Excel 2007 (.xlsx) formats respectively and because they’re the underlying libraries, they’re not limited to reading .xls and .xlsx formats. You can also read Thumb.db using POIFSFileSystem file format for example. You can download a sample project here.

Since you already know the difference between POIFSFileSystem and OPCPackage, you should understand what these two methods do. You are required to know what kind of document you are opening and then pass the file system to WorkbookFactory that in turn can automatically store a HSSFWorkbook or XSSFWorkbook depending on the file type. As these two classes implement IWorkbook interface, in most cases you do not need to care about what class instance it returns unless you use some advanced or specific Excel 2007 feature.

The third one is the most important part to introduce today – it does automatic inference, as long as you pass a Stream object it will know/infer whether it's .xls or .xlsx. Finally it returns the appropriate workbook instance.

The last one returns an IFormulaEvaluator that describes a formula object. Here a similar inference principle applies. Both HSSF and XSSF have formula calculation/evaluation classes, namely HSSFFormulaEvaluator and XSSFFormulaEvaluator.

OK we covered a little bit of code above but it was only abstractions, let’s see some real code:

using System;
using NPOI.SS.UserModel;
using System.IO;

class Program
{
    static void Main(string[] args)
    {
        if (args.Length < 1)
        {
            Console.WriteLine("missing argument: Excel file path (both 2003 and 2007 are supported)");
return; } using (FileStream fs = File.OpenRead(args[0])) { IWorkbook wb = WorkbookFactory.Create(fs);
Console.WriteLine("Value of Cell B2: " + wb.GetSheetAt(0).GetRow(1).GetCell(1)); }
Console.Read(); } }

This program assumes that the workbook file with a sheet exists on the file system and that the first sheet has a value in cell B2 or an error will be raised. The following is a screenshot of the workbook in question.

Excel Workbook with a single Sheet and value 5 in cell B2Figure 1 - Excel Workbook with a single Sheet and value 5 in Cell B2

Command line statement and test parameters were as follows:

Excel 2007: WorkbookFactoryDemo.exe demo2007.xlsx

Excel 2003: WorkbookFactoryDemo.exe demo2003.xls

The result should be equal the value of cell B2 = 5.

If we analyze the above program it seems we are not accessing anything from XSSF and HSSF namespaces, but in fact the wb is certainly an instance of HSSFWorkbook or XSSFWorkbook, that is, the real instance type/format is transparent to the user. This is where the IWorkbook interface shines.

For now you can study the source code here. Many NPOI 2.0 new features are not documented yet. So keep an eye on this blog for the next installment on this series.

NPOI 2.0 series of posts scheduled

NPOI logoWhat’s NPOI you may ask. So here it goes:

NPOI is the .NET version of POI Java project. POI is an open source project which can help you read/write Office 2003/2007 files.

NPOI advantages
a. It's totally free to use
b. Cover most Excel features (cell styles, data formats, formulas and so on)
c. Supports .xls, .xlsx, .docx
d. Designed to be interface-oriented (in NPOI.SS namespace)
e. Supports not only export but also import
f. .NET 2.0 based even for xlsx and docx (but it also supports .NET 4.0)
g. Successful use cases all over the world
h. Great amount of basic and to the point samples
i. Professional service support (even 24*7) directly from NPOI team (not free)

My dear friend Tony Qu from China (the guy behind NPOI development) approached me and asked me to help him share NPOI 2.0 news with the English speakers. I’m always glad to help spread the word about this amazing open source project that has served me in many situations.

The most seen and shared post of this humble blog as of now has almost 9000 shares and was published on 7/02/2009 – oh God that’s exactly 4.5 years ago… its title is Creating Excel spreadsheets .XLS and .XLSX in C#. This huge amount of shares shows that a tool that does what NPOI does is highly demanded in the market. No doubt!

You know: when I wrote that post I had no idea it would be so useful to so many people. My intention with this blog is really to share useful things with the world giving back a bit of what I find/learn/improve from the great minds spread throughout this big planet earth. This is a continuous\infinite process for sure.

OK dude: that’s enough of food for thought. Let me show you what’s going on with NPOI. Here you go…

NPOI 2.0 release
The release is planned for this January. We agreed to publish posts discussing major features/enhancements that’s arriving.

Here they are:

1 - WorkbookFactory can help make HSSF and XSSF transparent for users

2 - Excel 2007 and Word 2007 support

3 - Cell, Row, Sheet copying and Workbook merging

4 - Converting Excel XLS documents to HTML format

5 - isRightToLeft and setRightToLeft in XSSF and HSSF

6 - Diagonal line in cells for both Excel 2003 and Excel 2007

7 - Excel 2007 chart support

8 - New excel functions added in formula calculation

9 - NPOI tag added to files generated with NPOI

10 - XSSFWorkbook implements IEnumerator

Keep an eye on this blog for the next posts.

NPOI source code 
https://github.com/tonyqus/npoi

NPOI NuGet package
https://www.nuget.org/packages/NPOI/2.0.6

Getting ASP.NET MVC action exception message on jQuery $.ajax fail error callback

While testing an app I came across an error: I passed a string parameter to an action method responsible for loading an existing object by name on the database side. The problem is that there was no such object on the database. To make things even more difficult to handle this action method returns a PartialView that gets loaded in the view side using jQuery $.ajax. So how and what should I return to the caller if an exception happens while processing the request inside that action method? That’s a good question.

I tried something like this:

public ActionResult SubCatList(string subcat)
{
    var sub = Database.GetLogSubCategory(subcat);

    if(sub == null)
    {
        throw new EntryNotFoundException(String.Format("SubCategory {0} does not exist", subcat));
    }

    var subcatItems = Database.GetListItemsForSubCategory(sub);

    return PartialView(subcatItems);
}

My intention was to get that beautiful formatted exception message inside $.ajax fail callback using the error parameter like this:

$.ajax({
        url: '/api/subcatlist/subcatlist?subcat=' + subcat,
        type: 'GET',
        cache: false,
        success: function(html)
        {
            $("body").prepend(html);

// Do something more here...
} }).fail(function(xhr, textStatus, error) { displayMessage(error, 'danger'); }); }

This definitely DOES NOT WORK out of the box! So what’s the problem? The problem is that when checking/debugging the JavaScript using Firebug or Google Chrome,  the error parameter is set with a default IIS error message: Internal Server Error for the standard 500 error code. The exception message gets populated inside the xhr parameter instead in the middle of the standard page’s HTML code used by IIS.

Looking around I found something that could help in this case:

Handling Exceptions using jQuery and ASP.NET MVC

Improving the code shared on the above post, the basic idea is to have a common JsonResult action method in a BaseController for example that sets the Response.StatusDescription directly like this:

public JsonResult ThrowJsonError(Exception e)
{
    Logger.Error(e.Message, e);

    Response.StatusCode = (int)System.Net.HttpStatusCode.BadRequest;
    Response.StatusDescription = e.Message;

    return Json(new { Message = e.Message }, JsonRequestBehavior.AllowGet);
}

Now let’s use this beauty:

public ActionResult SubCatList(string subcat)
{
    var sub = Database.GetLogSubCategory(subcat);

    if(sub == null)
    {
        return ThrowJsonError(
            new EntryNotFoundException(String.Format("SubCategory {0} does not exist", subcat)));
    }

    var subcatItems = Database.GetListItemsForSubCategory(sub);

    return PartialView(subcatItems);
}

Now that beautiful formatted exception message is populated in the error parameter in $.ajax fail callback.

Objective achieved! Hope it’s useful it the future. Happy coding and 2014 for everyone…

Delete temporary file sent through StreamContent in ASP.NET Web API HttpResponseMessage

Recently I had to delete a temporary (temp) file/resource sent through HttpResponseMessage. The code I tried first was like this:

var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var response = new HttpResponseMessage();
                response.StatusCode = HttpStatusCode.OK;
                response.Content = new StreamContent(fileStream);
                response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                {
                    FileName = fileName
                };
File.Delete(filePath);
return response;

Fooling myself I didn’t notice that one cannot do File.Delete(filePath); before returning the file because it’ll be deleted from the server disk before arriving at the client – see that response.Content receives a StreamContent, that is, the file is yet to be streamed to the client – only after it’s been streamed is that we can delete the file.

This is the exception that was being logged:

20131028 18:29:10 435 [7299] ERROR com.itvizion.data.services.Controllers.DataTablesController - Unhandled exception during file action Could not find a part of the path 'C:\ITVizion\MyProject\FileExportService\Files\datatable.csv'.

Sure it could not find the file, it had already been deleted. D'oh

So how can we dispose a managed resource (in this case the file) after the client has successfully downloaded it? That’s a damn good question I made myself and Google yesterday.

Googling I found this StackOverflow question:

How to delete the file that was sent as StreamContent of HttpResponseMessage

The asker pointed me in the right direction. One has to inherit from HttpResponseMessage and override the Dispose method like this:

public class FileHttpResponseMessage : HttpResponseMessage
{
    private string filePath;

    public FileHttpResponseMessage(string filePath)
    {
        this.filePath = filePath;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        Content.Dispose();

        File.Delete(filePath);
    }
}

Note the order of the calls inside the Dispose method. This is an essential part that differs from the hint given by the SO question I mentioned above.

Now what’s left is put FileHttpResponseMessage to use like this:

var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);

var response = new FileHttpResponseMessage(filePath);
response.StatusCode = HttpStatusCode.OK;
response.Content = new StreamContent(fileStream);
response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
{
    FileName = fileName
};

return response;

FileHttpResponseMessage will take care of calling Dispose on the correct time deleting the temporary file from the server disk right after the client has finished downloading the file.

Cleaning up the server disk is a good practice after all. This can avoid future headaches.

Hope it helps.

Detecting and fixing Ajax.BeginForm and Partial View stale data

This blog post describes a technical hurdle and how to solve it with some (actually 2) clever lines of code.

In a recent ASP.NET MVC application called Vizion Logs, me and my buddy Mario Brenes from IT Vizion were facing a difficult situation: our Save action method returns a PartialView but the Model value passed to this partial view was not being displayed correctly under the covers, that is, the View was keeping the previous/stale Model value. This was completely ruining our business logic on the Controller side. This was a nasty problem because the first time the view/form is rendered the Model has an ID property set to -1. After post backing the Model and saving it we then pass this same Model to the partial view but now it has the ID property set to the ID automatically generated by an Oracle sequence defined in the database side. We count on this ID property value for subsequent save operations in which case this specific Model will be updated and not saved again as a new row in the database.

It’s worth mentioning that we use that Save action method to handle both Saving and Updating the log objects.

Here you can see the form being constructed with an Ajax.BeginForm on the View side:

@using(Ajax.BeginForm("Save", Model.Field,
    new AjaxOptions
    {
        HttpMethod = "POST",
        UpdateTargetId = "logs",
        InsertionMode = InsertionMode.Replace,
        OnFailure = "handleFailure",
        OnSuccess = "setupForm();parseFormForUnobtrusiveValidation();",
    }))

We did a Google Hangout to investigate what was going on… two minds working together to detect a problem is better than just one. It was Pair Programming (remotely) at its best. These hangouts with Mario are helping me to exercise my English too – WOOT! It’s a double gain or why not say a bargain!?
During this investigation Mario kept asking me questions that led us to the correct places in the code path. This allowed us to solve the problem in less than 30 minutes…

At first we tested the condition that was leading to the buggy path: saving the logs for the first time worked great but then hitting the save button a second time was causing duplicate entries on the database instead of updating the log entries already created.

As our Save method counts on the log’s ID property for subsequent saves (updates to be technically correct) it was fair enough to check the model that was being passed to the partial view right after hitting the save button for the first time. We saw that it had the ID property set correctly to the ID generated by the database. So this was the expected behavior. On the view side we use @Html.HiddenFor(l => l.ID) to keep the ID property as part of the post back values. This is important because with this we know when inside the Save action method that that specific log entry must be updated and not saved. A log entry must be created when it has an ID = -1 and it must be updated when ID != -1. Now what was left for us to check was see if the correct ID was present in the HTML generated by the partial view. Bingo: the ID property was still -1. Houston we have a problem. Now let us look at the jquery.unobtrusive-ajax.js file that handles the Ajax.BeginForm. What interest us on this JavaScript file is the asyncOnSuccess method:

function asyncOnSuccess(element, data, contentType) {
    var mode;

    if (contentType.indexOf("application/x-javascript") !== -1) {  // jQuery already executes JavaScript for us
        return;
    }

    mode = (element.getAttribute("data-ajax-mode") || "").toUpperCase();
$(element.getAttribute(
"data-ajax-update")).each(function (i, update) { var top; switch (mode) { case "BEFORE": top = update.firstChild; $("<div />").html(data).contents().each(function () { update.insertBefore(this, top); }); break; case "AFTER": $("<div />").html(data).contents().each(function () { update.appendChild(this); }); break; default: $(update).html(data); break; } }); }

We placed a breakpoint in this method and analyzed the data parameter that contains the HTML that’s going to be rendered by the partial view. To our surprise we saw that the ID property was still –1 despite it being 10 for example on the Controller side right before passing the model to the PartialView.

Jesus Christ… this is crazy! Confused smile How can that be the case?!

Well I thought… this sounds like a cache problem. This was the first time I was using the Ajax.BeginForm construct. Before using it I was used to use plain @Html.BeginForm and jquery $.ajax calls and as such I had never experienced this let’s say “bug” before. With this caching hint on mind I Googled about the problem and AS ALWAYS StackOverflow is our best friend. I found this question:

MVC Partial View Model Not Refreshing

jgauffin tells us to use the OutputCacheAttribute to disable cache for that specific action method and also clear the ModelState object. Something like this:

[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult Save(LogFormViewModel model)
{   
    // Custom Save/Update logic here...
    
    ModelState.Clear();
    
    return PartialView(model);
}

After decorating our Save action method with the OutputCache attribute and calling ModelState.Clear() we retested the form and to our surprise it just worked and we told each other: that’s great buddy!

We just solved the problem with some clever lines of code thanks to our collaborative thinking and to StackOverflow that never ceases to amaze me. For 99% of questions I have I can be sure that they’ve already been answered there – it’s just a matter of using the correct wording in a Google Search, but hold on: before searching you must first know why and what you need to search for. I think this is one lesson learned with this post.

That’s it. Hope this post is useful to the next soul/spirit/human being that might stumble on this.