Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Dapper.PartialUpdate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<Compile Remove="tests/**" />
<Compile Remove="examples/**" />
</ItemGroup>

</Project>
250 changes: 224 additions & 26 deletions DapperPartialExtensions.cs

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions Partial.cs
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
namespace Dapper.PartialUpdate;

/// <summary>
/// A wrapper struct that tracks whether a value has been explicitly set.
/// Used for partial update operations where only modified fields should be persisted.
/// </summary>
/// <typeparam name="T">The type of the wrapped value.</typeparam>
public struct Partial<T>
{
private T _value;

/// <summary>
/// Initializes a new instance of the <see cref="Partial{T}"/> struct with a value.
/// Sets <see cref="IsSet"/> to <c>true</c>.
/// </summary>
/// <param name="value">The value to wrap.</param>
public Partial(T value)
{
_value = value;
IsSet = true;
}

/// <summary>
/// Gets a value indicating whether this partial has been explicitly set.
/// </summary>
/// <value><c>true</c> if the value has been set; otherwise, <c>false</c>.</value>
public bool IsSet { get; private set; }

/// <summary>
/// Gets or sets the wrapped value. Setting the value automatically marks it as set.
/// </summary>
/// <value>The wrapped value.</value>
public T Value
{
readonly get => _value;
Expand All @@ -22,13 +40,27 @@ public T Value
}
}

/// <summary>
/// Unsets the value, marking it as not set. This causes the field to be omitted
/// from partial update operations.
/// </summary>
public void Unset()
{
_value = default!;
IsSet = false;
}

/// <summary>
/// Implicitly converts a value to a <see cref="Partial{T}"/>.
/// This allows direct assignment like <c>entity.Name = "Alice"</c>.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <returns>A new <see cref="Partial{T}"/> with the value and <see cref="IsSet"/> set to <c>true</c>.</returns>
public static implicit operator Partial<T>(T value) => new(value);

/// <summary>
/// Returns a string representation of this partial.
/// </summary>
/// <returns>The string representation of the value if set, otherwise "&lt;unset&gt;".</returns>
public override readonly string ToString() => IsSet ? $"{_value}" : "<unset>";
}
67 changes: 60 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ Use this when you need patch-style updates/inserts:

`Partial<T>` tracks whether a value has been set (`IsSet`), and extensions build SQL using only those set fields.

## Features

- **Partial Updates**: Update only the fields you've explicitly set
- **Partial Inserts**: Insert only the fields you've set, or use DEFAULT VALUES
- **Database Agnostic**: Supports SQL Server, SQLite, PostgreSQL, and MySQL with automatic identifier quoting
- **Async Support**: Full async/await support for all operations
- **Comprehensive Testing**: Unit tests and SQLite integration tests included
- **Example Project**: Ready-to-run SQLite example demonstrating all features

## Install

```bash
Expand Down Expand Up @@ -58,6 +67,29 @@ You can also assign directly with implicit conversion:
entity.Name = "Alice"; // Partial<string> IsSet=true
```

## Database Type Support

The library supports multiple database types with appropriate identifier quoting:

```csharp
// SQL Server (default, uses [brackets])
connection.UpdatePartials(entity);

// SQLite, PostgreSQL (uses "double quotes")
connection.UpdatePartials(entity, DatabaseType.Standard);

// MySQL (uses `backticks`)
connection.UpdatePartials(entity, DatabaseType.MySql);
```

### Identifier Quoting

- **SQL Server**: `[TableName]`, `[ColumnName]`
- **SQLite/PostgreSQL**: `"TableName"`, `"ColumnName"`
- **MySQL**: `` `TableName` ``, `` `ColumnName` ``

The library automatically escapes special characters in identifiers.

## Extension Methods

`DapperPartialExtensions` provides:
Expand All @@ -66,18 +98,39 @@ entity.Name = "Alice"; // Partial<string> IsSet=true
- `InsertPartials<T>(...)`
- `InsertPartialsAsync<T>(...)`

All methods have overloads that accept a `DatabaseType` parameter for database-specific identifier quoting.

### Update behavior

- Table name from `[Table]` or class name.
- Key from `[Key]`, or `Id`, or `{TypeName}Id`.
- Column names from `[Column]` or property name.
- Only `Partial<T>` properties with `IsSet == true` are updated.
- If no fields are set, update returns `0` without executing SQL.
- Table name from `[Table]` or class name
- Key from `[Key]`, or `Id`, or `{TypeName}Id`
- Column names from `[Column]` or property name
- Only `Partial<T>` properties with `IsSet == true` are updated
- If no fields are set, update returns `0` without executing SQL
- Improved error messages with entity and property names

### Insert behavior

- Only set `Partial<T>` properties are included.
- If no partial fields are set, executes `INSERT ... DEFAULT VALUES`.
- Only set `Partial<T>` properties are included
- If no partial fields are set, executes `INSERT ... DEFAULT VALUES`

## Examples

See the `examples/SQLiteExample` directory for a complete working example using SQLite.

## Testing

Run the test suite:

```bash
cd tests/Dapper.PartialUpdate.Tests
dotnet test
```

The test suite includes:
- Unit tests for `Partial<T>` wrapper
- Database type quoting tests
- SQLite integration tests for insert and update operations

## Build NuGet Package (local)

Expand Down
126 changes: 126 additions & 0 deletions examples/SQLiteExample/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using System.Data;
using Dapper;
using Dapper.PartialUpdate;
using Microsoft.Data.Sqlite;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

// This example demonstrates how to use Dapper.PartialUpdate with SQLite
// to perform partial updates and inserts.

Console.WriteLine("=== Dapper.PartialUpdate SQLite Example ===\n");

// Create an in-memory SQLite database
using var connection = new SqliteConnection("Filename=:memory:");
connection.Open();

// Initialize the database schema
connection.Execute(@"
CREATE TABLE Products (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT,
Price REAL,
Stock INTEGER,
Description TEXT
);
");

Console.WriteLine("Database initialized with Products table.\n");

// Example 1: Insert with only some fields set
Console.WriteLine("1. Inserting a product with only Name and Price:");
var newProduct = new Product();
newProduct.Name = "Laptop";
newProduct.Price = 999.99;
// Note: Stock and Description are not set, so they won't be inserted

connection.InsertPartials(newProduct, DatabaseType.Standard);
Console.WriteLine($" Inserted product with Id = {newProduct.Id}");

// Verify the insert
var inserted = connection.QuerySingle<Product>("SELECT * FROM Products WHERE Id = @Id", new { Id = newProduct.Id });
Console.WriteLine($" Name: {inserted.NameValue}, Price: {inserted.PriceValue}, Stock: {inserted.StockValue ?? 0}, Description: {inserted.DescriptionValue ?? "(null)"}\n");

// Example 2: Insert with all fields set
Console.WriteLine("2. Inserting a product with all fields:");
var product2 = new Product();
product2.Name = "Mouse";
product2.Price = 29.99;
product2.Stock = 100;
product2.Description = "Wireless optical mouse";

connection.InsertPartials(product2, DatabaseType.Standard);
Console.WriteLine($" Inserted product with Id = {product2.Id}");

// Verify the insert
var inserted2 = connection.QuerySingle<Product>("SELECT * FROM Products WHERE Id = @Id", new { Id = product2.Id });
Console.WriteLine($" Name: {inserted2.NameValue}, Price: {inserted2.PriceValue}, Stock: {inserted2.StockValue}, Description: {inserted2.DescriptionValue}\n");

// Example 3: Update only specific fields
Console.WriteLine("3. Updating only the Stock field of the first product:");
var productToUpdate = connection.QuerySingle<Product>("SELECT * FROM Products WHERE Id = 1");
productToUpdate.Stock = 50; // Only set Stock, leave other fields unset

connection.UpdatePartials(productToUpdate, DatabaseType.Standard);
Console.WriteLine(" Updated Stock to 50");

// Verify the update
var updated = connection.QuerySingle<Product>("SELECT * FROM Products WHERE Id = 1");
Console.WriteLine($" Name: {updated.NameValue}, Price: {updated.PriceValue}, Stock: {updated.StockValue}, Description: {updated.DescriptionValue ?? "(null)"}\n");

// Example 4: Update multiple fields at once
Console.WriteLine("4. Updating Price and Description of the second product:");
var productToUpdate2 = connection.QuerySingle<Product>("SELECT * FROM Products WHERE Id = 2");
productToUpdate2.Price = 24.99;
productToUpdate2.Description = "Ergonomic wireless mouse";

connection.UpdatePartials(productToUpdate2, DatabaseType.Standard);
Console.WriteLine(" Updated Price to 24.99 and Description");

// Verify the update
var updated2 = connection.QuerySingle<Product>("SELECT * FROM Products WHERE Id = 2");
Console.WriteLine($" Name: {updated2.NameValue}, Price: {updated2.PriceValue}, Stock: {updated2.StockValue}, Description: {updated2.DescriptionValue}\n");

// Example 5: Using async methods
Console.WriteLine("5. Using async methods to insert a new product:");
var product3 = new Product();
product3.Name = "Keyboard";
product3.Price = 79.99;
product3.Stock = 25;

await connection.InsertPartialsAsync(product3, DatabaseType.Standard);
Console.WriteLine($" Inserted product with Id = {product3.Id}");

// Example 6: Insert with no fields set (uses DEFAULT VALUES)
Console.WriteLine("6. Inserting a product with no fields set:");
var emptyProduct = new Product();
connection.InsertPartials(emptyProduct, DatabaseType.Standard);
Console.WriteLine($" Inserted empty product with Id = {emptyProduct.Id}");

// Verify all products
Console.WriteLine("\n=== All Products in Database ===");
var allProducts = connection.Query<Product>("SELECT * FROM Products").ToList();
foreach (var p in allProducts)
{
Console.WriteLine($" Id: {p.Id}, Name: {p.NameValue ?? "(null)"}, Price: {p.PriceValue ?? 0}, Stock: {p.StockValue ?? 0}, Description: {p.DescriptionValue ?? "(null)"}");
}

Console.WriteLine("\n=== Example Complete ===");

[Table("Products")]
public class Product
{
[Key]
public int Id { get; set; }

public Partial<string> Name { get; set; }
public Partial<double> Price { get; set; }
public Partial<long> Stock { get; set; }
public Partial<string> Description { get; set; }

// Helper properties for reading values in the example
public string? NameValue => Name.IsSet ? Name.Value : null;
public double? PriceValue => Price.IsSet ? Price.Value : null;
public long? StockValue => Stock.IsSet ? Stock.Value : null;
public string? DescriptionValue => Description.IsSet ? Description.Value : null;
}
19 changes: 19 additions & 0 deletions examples/SQLiteExample/SQLiteExample.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.10" />
<PackageReference Include="Dapper" Version="2.1.66" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Dapper.PartialUpdate.csproj" />
</ItemGroup>

</Project>
Loading