Getting started with ADO.NET - Insert and Query against SQLite Database

Getting started with ADO.NET - Insert and Query against SQLite Database

This post is a short demo of using ADO.NET to insert and query data against a SQLite database.

The source code is available on GitHub here.

Context

The example data are achievements - anything that can be quantified and occurs on a certain date:

public sealed class Achievement
{
    public long Id { get; set; }
    public AchievementClass AchievementClass { get; set; }
    public DateOnly CompletedDate { get; set; }
    public int Quantity { get; set; }
}

Different kinds of achievements are identified by their name and a unit for interpreting quantities:

public sealed class AchievementClass
{
    public long Id { get; set; }
    public string Name { get; set; }
    public string Unit { get; set; }
}

Implementation

Step 1 - Configure

The first commit configures the project and creates the database upon repository initialisation.

SQLite databases reside on the local file system. The snippet below places the database inside the application data special folder (on MacOS called "Application Support"), which is the standard location for storing application-specific data and configuration files. This way, one does not need to worry about differences between operating systems or how to use relative paths.

It also uses the environment name as part of the path. This ensures that development and production databases are different. The environment is configured by setting the DOTNET_ENVIRONMENT environment variable to Deveploment.

public Repository(IHostEnvironment hostEnvironment)
{
	string dataSource = Path.Combine(
		Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
		"AdoDotNetDemo",
		hostEnvironment.EnvironmentName,
		"data.db");
	Directory.CreateDirectory(Path.GetDirectoryName(dataSource)!);
	_connectionString = $"Data Source={dataSource}";
}

When the code is run, it creates the relevant folders:

Step 2 - Ping

The second commit implements the ping method:

await using SqliteConnection connection = new(_connectionString);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = "SELECT 1";
await connection.OpenAsync();
Object? output = await command.ExecuteScalarAsync();
return output != null && (long)output == 1;

Every method instantiates its own connection.

By default, integer values have 64 bits, i.e. they correspond to the long data type in C#. In particular, casting output to int fails.

The using declaration ensures that the connection and the command are disposed at the end of the method.

At this point, running the code also creates the data.db file:

Step 3 - Setup

The third commit sets up the Achievement and AchievementClass tables, using the following query:

BEGIN TRANSACTION;

CREATE TABLE IF NOT EXISTS AchievementClass (
  Id INTEGER PRIMARY KEY AUTOINCREMENT,
  Name TEXT NOT NULL,
  Unit TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS Achievement (
  Id INTEGER PRIMARY KEY AUTOINCREMENT,
  AchievementClassId INTEGER NOT NULL,
  CompletedDate TEXT NOT NULL,
  Quantity INTEGER NOT NULL,
  RecordedDate TEXT NOT NULL,
  FOREIGN KEY (AchievementClassId) REFERENCES AchievementClass(Id)
);

COMMIT;

Step 4 - Insert

The fourth commit inserts achievement classes:

await using SqliteConnection connection = new(_connectionString);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
					  INSERT INTO AchievementClass (Name, Unit) VALUES (@Name, @Unit);
					  SELECT last_insert_rowid() AS Id;
					  """;
command.Parameters.AddWithValue("@Name", achievementClass.Name);
command.Parameters.AddWithValue("@Unit", achievementClass.Unit);
await connection.OpenAsync();
object? output = await command.ExecuteScalarAsync();
if (output is null) return null;
achievementClass.Id = (long)output;
return achievementClass;

and achievements:

await using SqliteConnection connection = new(_connectionString);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
					  INSERT INTO Achievement (AchievementClassId, CompletedDate, Quantity, RecordedDate)
					  VALUES (@AchievementClassId, @CompletedDate, @Quantity, datetime('now'));
					  SELECT last_insert_rowid() AS Id;
					  """;
command.Parameters.AddWithValue("@AchievementClassId", achievement.AchievementClass.Id);
command.Parameters.AddWithValue("@CompletedDate", achievement.CompletedDate);
command.Parameters.AddWithValue("@Quantity", achievement.Quantity);
await connection.OpenAsync();
object? output = await command.ExecuteScalarAsync();
if (output is null) return null;
achievement.Id = (long)output;
return achievement;

Both inserts are nearly identical in their implementations.

There is no need to insert primary keys because they are automatically created through auto-increment.

The function last_insert_rowid() is a function specific to SQLite (rather than SQL in general). It does exactly what it says, i.e. returns the id of the last inserted row.

Step 5 - Query

The fifth commit queries all achievements for a specific class, which is achieved by filtering on AchievementClassId:

await using SqliteConnection connection = new(_connectionString);
await using SqliteCommand command = connection.CreateCommand();
command.CommandText = """
					  SELECT Id, CompletedDate, Quantity
					  FROM Achievement
					  WHERE AchievementClassId = @AchievementClassId
					  """;
command.Parameters.AddWithValue("@AchievementClassId", achievementClass.Id);
await connection.OpenAsync();
SqliteDataReader reader = await command.ExecuteReaderAsync();
List<Achievement> achievements = new();
while (await reader.ReadAsync())
{
	long id = reader.GetInt64(reader.GetOrdinal("Id"));
	DateTime completedDate = reader.GetDateTime(reader.GetOrdinal("CompletedDate"));
	DateOnly completedDateOnly = DateOnly.FromDateTime(completedDate);
	int quantity = reader.GetInt32(reader.GetOrdinal("Quantity"));
	Achievement achievement = new()
	{
		Id = id,
		AchievementClass = achievementClass,
		CompletedDate = completedDateOnly,
		Quantity = quantity,
	};
	achievements.Add(achievement);
}

return achievements.ToArray();

Running the program results in the following output:

Can access database: True
Completed setup: True
Inserted achievement class: AchievementClass { Id: 1, Name: Drink water, Unit: Glass }
Successfully inserted achievements
Querying achievements
Found achievement: Achievement { Id: 1, Class: Drink water, Quantity: 2 Unit: Glass }
Found achievement: Achievement { Id: 2, Class: Drink water, Quantity: 1 Unit: Glass }
Found achievement: Achievement { Id: 3, Class: Drink water, Quantity: 1 Unit: Glass }

Read more