Tiny PowerShell Project 6 - LiteDB and LINQ Queries

Tiny PowerShell Project 6 - LiteDB and LINQ Queries

A lightweight, serverless, document-based NoSQL database

All tutorials that I found online gave outdated instructions on how to download LiteDB for PowerShell. Learning how to download and set it up alone can make up for a tiny project. First, let's see how to download the LiteDB.dll file manually:

  1. Visit the LiteDB NuGet package page: https://www.nuget.org/packages/LiteDB/

  2. Click on the "Download Package" link on the right side of the page, under the "Info" section.

  3. Extract the downloaded .nupkg file using an archive tool (e.g., 7-Zip). You can rename the file to .zip and extract it.

  4. Navigate to the "lib" folder in the extracted contents, and find the appropriate LiteDB.dll file for your environment (e.g., "netstandard2.0" for cross-platform compatibility).

  5. Copy the LiteDB.dll file to a location on your computer.

Now that you have the LiteDB.dll file, you can reference it in your PowerShell script:

# Load LiteDB assembly
Add-Type -Path "path\to\LiteDB.dll"

Replace "path\to\LiteDB.dll" with the actual path to the LiteDB.dll file you downloaded.

You'll find two versions of this dll, so let's review the difference between net45 and netstandard1.3:

net45 and netstandard1.3 are different target frameworks within the .NET ecosystem. They represent different versions of the .NET Framework and .NET Standard. Here's a brief overview of each:

  1. net45:

net45 targets .NET Framework 4.5, which is a specific version of the .NET Framework. The .NET Framework is a Windows-only runtime and development framework for building Windows applications. It has been the primary .NET implementation for many years but has been superseded by .NET Core and .NET 5+ for new projects.

  1. netstandard1.3:

netstandard1.3 targets .NET Standard 1.3, which is a specification that represents a set of APIs available across different .NET implementations, such as .NET Framework, .NET Core, and Xamarin. The purpose of .NET Standard is to establish uniformity and compatibility across different .NET platforms, making it easier to create libraries and components that work with multiple .NET implementations.

By targeting .NET Standard 1.3, a library can be used in projects that target .NET Framework 4.6 or higher, .NET Core 1.0 or higher, and other compatible .NET implementations.

In summary, net45 targets a specific version of the .NET Framework (4.5), which is Windows-only, while netstandard1.3 targets a version of the .NET Standard, enabling compatibility with a broader range of .NET implementations, including .NET Framework 4.6+, .NET Core, and Xamarin. I decided to go with net45 because my operating system is Windows.

Now let's see how we can do CRUD:

# Load LiteDB assembly
Add-Type -Path "path\to\LiteDB.dll"

# Define a simple data class
class Person {
    [int] $Id
    [string] $Name
    [int] $Age
}

# Create a new Person object
$newPerson = [Person]::new()
$newPerson.Name = "John Doe"
$newPerson.Age = 30

# Create or open a LiteDB database file
$connectionString = "Filename=myDatabase.db;Mode=Shared"
$database = [LiteDB.LiteDatabase]::new($connectionString)

# Get the 'people' collection
$people = $database.GetCollection("people")

# Convert the Person object to a BsonDocument
$mapper = [LiteDB.BsonMapper]::Global
$personBson = $mapper.ToDocument($newPerson)

# Insert a new person (Create)
$insertResult = $people.Insert($personBson)

# Get the inserted ID
$insertedId = $personBson["_id"]

# Find a person by ID (Read)
$foundPersonBson = $people.FindById($insertedId)
$foundPerson = $mapper.ToObject([Person], $foundPersonBson)

# Update a person's age (Update)
$foundPerson.Age = 35
$updatedPersonBson = $mapper.ToDocument($foundPerson)
$people.Update($updatedPersonBson)

# Delete a person by ID (Delete)
$people.Delete($insertedId)

# Close the database
$database.Dispose()

In this example, I added the steps to convert the Person object to a BsonDocument using the $mapper.ToDocument($newPerson) method. Then, I used the $people.Insert($personBson) method to insert the object. Similarly, I updated the methods for reading and updating the object using the BsonMapper.

Armed with what we just learned, let's do a more complicated example that would make up for our tiny project. this example around these entities: environments, computers, components, and tokens an environment can have many computers. a computer can have one component. A component can have many tokens.

In this example, I've created classes for each entity and set up relationships between them as described. Note that for simplicity, I'm storing each entity type in a separate collection in the LiteDB database. You can adjust the example according to your requirements.

# Load LiteDB assembly
Add-Type -Path "path\to\LiteDB.dll"

# Define data classes
class Environment {
    [int] $Id
    [string] $Name
}

class Computer {
    [int] $Id
    [string] $Name
    [int] $EnvironmentId
}

class Component {
    [int] $Id
    [string] $Name
    [int] $ComputerId
}

class Token {
    [int] $Id
    [string] $Value
    [int] $ComponentId
}

# Create or open a LiteDB database file
$connectionString = "Filename=myDatabase.db;Mode=Shared"
$database = [LiteDB.LiteDatabase]::new($connectionString)

# Get collections for each entity type
$environments = $database.GetCollection("environments")
$computers = $database.GetCollection("computers")
$components = $database.GetCollection("components")
$tokens = $database.GetCollection("tokens")

# Create objects for each entity type
$env = [Environment]::new()
$env.Name = "Production"

$comp = [Computer]::new()
$comp.Name = "Server01"

$component = [Component]::new()
$component.Name = "WEB"

$token = [Token]::new()
$token.Value = "Token1"

# Insert the environment object
$mapper = [LiteDB.BsonMapper]::Global
$envBson = $mapper.ToDocument($env)
$environments.Insert($envBson)
$env.Id = $envBson["_id"]

# Insert the computer object and associate it with the environment
$comp.EnvironmentId = $env.Id
$compBson = $mapper.ToDocument($comp)
$computers.Insert($compBson)
$comp.Id = $compBson["_id"]

# Insert the component object and associate it with the computer
$component.ComputerId = $comp.Id
$componentBson = $mapper.ToDocument($component)
$components.Insert($componentBson)
$component.Id = $componentBson["_id"]

# Insert the token object and associate it with the component
$token.ComponentId = $component.Id
$tokenBson = $mapper.ToDocument($token)
$tokens.Insert($tokenBson)
$token.Id = $tokenBson["_id"]

# Close the database
$database.Dispose()

Now how do we find find the value of a token based on the environment, computer name, and component. Before our show you this, let's do a little dive into what LINQ query is.

LINQ, which stands for Language-Integrated Query, is a feature introduced in .NET Framework 3.5 (circa 2007) that allows developers to perform query operations directly within their .NET programming languages like C# and VB.NET. It offers a consistent model for working with data across various kinds of data sources and formats.

  1. Integrated into C# and VB.NET: LINQ is integrated into these languages, allowing for a fluent and easy-to-read syntax. It enables querying directly within the programming language without the need for string-based query expressions (which you typically see in SQL).

  2. Provider-based: LINQ is designed to be extensible. That means while you can use LINQ to query collections in memory (like Lists or Arrays), you can also use it to query other data sources, such as relational databases (with LINQ to SQL or Entity Framework), XML documents (LINQ to XML), or even remote web services.

  3. Deferred Execution: One of the key characteristics of LINQ is its deferred execution model. When you define a LINQ query, the query is not executed until you iterate over the results. This can be beneficial in terms of performance and flexibility.

  4. Strongly Typed: LINQ works with strongly typed data. This means that if you're querying a database, for instance, the results come back as strongly typed objects rather than generic data rows. This provides compile-time checking and IntelliSense support in the IDE.

  5. Lambda Expressions: LINQ often makes use of lambda expressions in C#, which provide a concise way to express functions or methods inline.

LINQ provides a powerful and consistent way to work with data, and over the years, it has become an integral part of .NET development. It allows developers to work with data more abstractly, insulating them (to a degree) from the specifics of the underlying data source.

Now that we know what LINQ is, lets work on our query. This query will use the LiteDB Find method with LINQ queries to retrieve the required data:

# Load LiteDB assembly
Add-Type -Path "path\to\LiteDB.dll"

# Function to find a token value based on environment, computer name, and component
function Find-TokenValue {
    param (
        [string] $environmentName,
        [string] $computerName,
        [string] $componentName
    )

    # Open the LiteDB database file
    $connectionString = "Filename=myDatabase.db;Mode=Shared"
    $database = [LiteDB.LiteDatabase]::new($connectionString)

    # Get collections for each entity type
    $environments = $database.GetCollection("environments")
    $computers = $database.GetCollection("computers")
    $components = $database.GetCollection("components")
    $tokens = $database.GetCollection("tokens")

    # LINQ queries to find the target environment, computer, component, and token
    $targetEnvironment = $environments.Find([LiteDB.Query]::EQ("Name", $environmentName)).FirstOrDefault()
    if ($null -ne $targetEnvironment) {
        $targetComputer = $computers.Find(([LiteDB.Query]::EQ("EnvironmentId", $targetEnvironment["_id"])) -and ([LiteDB.Query]::EQ("Name", $computerName))).FirstOrDefault()
        if ($null -ne $targetComputer) {
            $targetComponent = $components.Find(([LiteDB.Query]::EQ("ComputerId", $targetComputer["_id"])) -and ([LiteDB.Query]::EQ("Name", $componentName))).FirstOrDefault()
            if ($null -ne $targetComponent) {
                $targetToken = $tokens.Find([LiteDB.Query]::EQ("ComponentId", $targetComponent["_id"])).FirstOrDefault()
                if ($null -ne $targetToken) {
                    # Return the token value
                    return $targetToken["Value"]
                }
            }
        }
    }

    # Return $null if not found
    return $null
}

# Example usage
$tokenValue = Find-TokenValue -environmentName "Production" -computerName "Server01" -componentName "WEB"
Write-Host "Token value: $tokenValue"

This example defines a function called Find-TokenValue that takes the environment name, computer name, and component name as input. It opens the LiteDB database, gets the collections for each entity type, and uses LINQ queries with LiteDB.Query methods to find the corresponding environment, computer, and component objects. If it finds the matching token object, it returns the token value. If any of the provided parameters don't match an existing object, the function returns $null.

Did you find this article valuable?

Support Application Support by becoming a sponsor. Any amount is appreciated!