Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ dotnet_diagnostic.RCS1227.severity = none
# RCS1124: Inline local variable
dotnet_diagnostic.RCS1124.severity = suggestion

# IDE0290: Use primary constructor
dotnet_diagnostic.IDE0290.severity = none

[*.{cs,vb}]
#### Naming styles ####

Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/) and
this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]
## [1.1.0] - TODO: Add date of release

### Added
- Fluent configuration API for general settings and entity mappings (Fixes [issue #3](https://github.com/rent-a-developer/DbConnectionPlus/issues/3))
- Support for column name mapping via System.ComponentModel.DataAnnotations.Schema.ColumnAttribute (Fixes [issue #1](https://github.com/rent-a-developer/DbConnectionPlus/issues/1))
- Throw helper for common exceptions

### Changed
- Updated all dependencies to latest stable versions
- Refactored unit and integration tests for better maintainability

## [1.0.0] - 2026-01-24

Expand Down
117 changes: 85 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ All examples in this document use SQL Server.
- [EnumSerializationMode](#enumserializationmode)
- [InterceptDbCommand](#interceptdbcommand)
- [Entity Mapping](#entity-mapping)
- [Fluent API](#fluent-api)
- [Data annotation attributes](#data-annotation-attributes)
- [General-purpose methods](#general-purpose-methods)
- [ExecuteNonQuery / ExecuteNonQueryAsync](#executenonquery--executenonqueryasync)
Expand Down Expand Up @@ -326,18 +327,9 @@ Configuration:
- [EnumSerializationMode](#enumserializationmode) - Configure how enum values are serialized when sent to the database
- [InterceptDbCommand](#interceptdbcommand) - Configure a delegate to intercept `DbCommand`s executed by DbConnectionPlus

Entity mapping:
Data annotation attributes:
- [`System.ComponentModel.DataAnnotations.Schema.TableAttribute`](#systemcomponentmodeldataannotationsschematableattribute)
Specify the name of the table where entities of an entity type are stored
- [`System.ComponentModel.DataAnnotations.Schema.ColumnAttribute`](#systemcomponentmodeldataannotationsschemacolumnattribute)
Specify the name of the column where a property of an entity type is stored
- [`System.ComponentModel.DataAnnotations.KeyAttribute`](#systemcomponentmodeldataannotationskeyattribute)
Specify the key property / properties of an entity type
- [`System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedAttribute`](#systemcomponentmodeldataannotationsschemadatabasegeneratedattribute)
Specify that a property of an entity type is generated by the database
- [`System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute`](#systemcomponentmodeldataannotationsschemanotmappedattribute)
Specify that a property of an entity type is not mapped to a column in the database
Entity mapping:
- [Fluent API](#fluent-api) - Configure entity mapping via fluent API
- [Data annotation attributes](#data-annotation-attributes) - Configure entity mapping via data annotation attributes

General-purpose methods:
- [ExecuteNonQuery / ExecuteNonQueryAsync](#executenonquery--executenonqueryasync) - Execute a non-query and return
Expand Down Expand Up @@ -374,11 +366,28 @@ it inside an SQL statement

### Configuration

Use `DbConnectionExtensions.Configure` to configure DbConnectionPlus.

```csharp
using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;

DbConnectionExtensions.Configure(config =>
{
// Configuration options go here
});
```

> [!NOTE]
> `DbConnectionExtensions.Configure` can only be called once.
> After it has been called the configuration of DbConnectionPlus is frozen and cannot be changed anymore.

#### EnumSerializationMode
Use `DbConnectionExtensions.EnumSerializationMode` to configure how enum values are serialized when they are sent to a
database.
Use `EnumSerializationMode` to configure how enum values are serialized when they are sent to a database.
The default value is `EnumSerializationMode.Strings`, which serializes enum values as their string representation.

When `EnumSerializationMode` is set to `EnumSerializationMode.Strings`, enum values are serialized as strings.
When `EnumSerializationMode` is set to `EnumSerializationMode.Integers`, enum values are serialized as integers.

```csharp
using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;

Expand All @@ -404,43 +413,84 @@ var user = new User
Role = UserRole.User
};

DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Strings;
DbConnectionExtensions.Configure(config =>
{
config.EnumSerializationMode = EnumSerializationMode.Strings;
});

connection.InsertEntity(user); // Column "Role" will contain the string "User".

DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers;
DbConnectionExtensions.Configure(config =>
{
config.EnumSerializationMode = EnumSerializationMode.Integers;
});

connection.InsertEntity(user); // Column "Role" will contain the integer 2.
```

When `DbConnectionExtensions.EnumSerializationMode` is set to `EnumSerializationMode.Strings`, enum values are
serialized as strings.
When `DbConnectionExtensions.EnumSerializationMode` is set to `EnumSerializationMode.Integers`, enum values are
serialized as integers.

#### InterceptDbCommand
Use `DbConnectionExtensions.InterceptDbCommand` to configure a delegate that intercepts a `DbCommand` before it is
executed. This can be useful for logging, modifying the command text, or applying additional configuration.
Use `InterceptDbCommand` to configure a delegate that intercepts a `DbCommand` before it is executed. This can be
useful for logging, modifying the command text, or applying additional configuration.

```csharp
using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;

DbConnectionExtensions.InterceptDbCommand = (dbCommand, temporaryTables) =>
DbConnectionExtensions.Configure(config =>
{
// Log the command text
Console.WriteLine("Executing SQL Command: " + dbCommand.CommandText);
config.InterceptDbCommand = (dbCommand, temporaryTables) =>
{
// Log the command text
Console.WriteLine("Executing SQL Command: " + dbCommand.CommandText);

// Modify the command text if needed
dbCommand.CommandText += " OPTION (RECOMPILE)";
// Modify the command text if needed
dbCommand.CommandText += " OPTION (RECOMPILE)";

// Apply additional configuration if needed
dbCommand.CommandTimeout = 60;
};
// Apply additional configuration if needed
dbCommand.CommandTimeout = 60;
};
});
```

See [DbCommandLogger](https://github.com/rent-a-developer/DbConnectionPlus/blob/main/tests/DbConnectionPlus.IntegrationTests/TestHelpers/DbCommandLogger.cs)
for an example of logging executed commands.

#### Entity Mapping

You can configure how entity types are mapped to database tables and columns using either the fluent API or data
annotation attributes.

> [!NOTE]
> Mapping configured via the fluent API takes precedence over mapping configured via data annotation attributes.
> When a fluent mapping exist for an entity type, the data annotations on this entity type are ignored.
> When a fluent mapping exists for an entity property, the data annotations on this property are ignored.

##### Fluent API
You can use the fluent API to configure how entity types are mapped to database tables and columns.

```csharp
using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;

DbConnectionExtensions.Configure(config =>
{
config.Entity<Product>()
.ToTable("Products");

config.Entity<Product>()
.Property(a => a.Id)
.HasColumnName("ProductId");
.IsIdentity()
.IsKey();

config.Entity<Product>()
.Property(a => a.DiscountedPrice)
.IsComputed();

config.Entity<Product>()
.Property(a => a.IsOnSale)
.IsIgnored();
});
```

##### Data annotation attributes

You can use the following attributes to configure how entity types are mapped to database tables and columns:
Expand Down Expand Up @@ -1081,7 +1131,10 @@ Then register your custom database adapter before using DbConnectionPlus:
```csharp
using RentADeveloper.DbConnectionPlus.DatabaseAdapters;

DatabaseAdapterRegistry.RegisterAdapter<MyConnectionType>(new MyDatabaseAdapter());
DbConnectionExtensions.Configure(config =>
{
config.RegisterDatabaseAdapter<MyConnectionType>(new MyDatabaseAdapter());
});
```

See [SqlServerDatabaseAdapter](https://github.com/rent-a-developer/DbConnectionPlus/blob/main/src/DbConnectionPlus/DatabaseAdapters/SqlServer/SqlServerDatabaseAdapter.cs)
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/DbConnectionPlus.Benchmarks/Benchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1421,7 +1421,7 @@ public void UpdateEntities_Manually()

for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++)
{
var updatedEntities = Generate.UpdatesFor(this.entitiesInDb);
var updatedEntities = Generate.UpdateFor(this.entitiesInDb);

using var command = connection.CreateCommand();
command.CommandText = """
Expand Down Expand Up @@ -1547,7 +1547,7 @@ public void UpdateEntities_DbConnectionPlus()

for (var i = 0; i < UpdateEntities_OperationsPerInvoke; i++)
{
var updatesEntities = Generate.UpdatesFor(this.entitiesInDb);
var updatesEntities = Generate.UpdateFor(this.entitiesInDb);

connection.UpdateEntities(updatesEntities);
}
Expand Down
99 changes: 83 additions & 16 deletions docs/DESIGN-DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# DbConnectionPlus - Design Decisions Document

**Version:** 1.0.0
**Version:** 1.1.0
**Last Updated:** January 2026
**Author:** David Liebeherr

Expand Down Expand Up @@ -579,6 +579,45 @@ public static InterpolatedParameter Parameter(

## Entity Mapping Strategy

### Fluent API-based Configuration

**Decision:** Provide optional fluent API for entity configuration.

**Example:**
```csharp
DbConnectionExtensions.Configure(config =>
{
// Table name mapping:
config.Entity<Product>()
.ToTable("Products");

// Column name mapping:
config.Entity<Product>()
.Property(a => a.Name)
.HasColumnName("ProductName");

// Key column mapping:
config.Entity<Product>()
.Property(a => a.Id)
.IsKey();

// Database generated column mapping:
config.Entity<Product>()
.Property(a => a.DiscountedPrice)
.IsDatabaseGenerated();

// Ignored property mapping:
config.Entity<Product>()
.Property(a => a.IsOnSale)
.Ignore();
}
);
```

**Benefits:**
- **Mostly EF Core compatible**: Similar API as of EF core
- **Convenient**: Provides convinient way to configure entities without attributes

### Attribute-Based Configuration

**Decision:** Use standard .NET data annotations for entity metadata.
Expand Down Expand Up @@ -631,13 +670,15 @@ public static class EntityHelper
```

**Cached Information:**
- Table name (from `[Table]` attribute or type name)
- Table name (from `[Table]` attribute, fluent API config or type name)
- Metadata of properties:
- Mapped properties (excluding `[NotMapped]`)
- Key properties (marked with `[Key]`)
- Mapped properties (excluding ignored properties)
- Key properties
- Computed properties
- Identity property
- Database generated properties
- Insert properties (properties to be included when inserting an entity)
- Update properties (properties to be included when updating an entity)
- Database generated properties (marked with `[DatabaseGenerated(DatabaseGeneratedOption.Identity)]` or `[DatabaseGenerated(DatabaseGeneratedOption.Computed)]`)

**Performance Impact:**
- First entity operation: few ms for metadata extraction
Expand Down Expand Up @@ -996,22 +1037,45 @@ public List<Product> Query_Entities_DbConnectionPlus()

### Global Configuration

**Decision:** Use static properties for global settings that rarely change.
**Decision:** Provide a config method for configuring global settings.

**Current Settings:**
**Best Practice:**
Set during application startup before any database operations:
```csharp
public static class DbConnectionExtensions
// In Program.cs or Startup.cs

DbConnectionExtensions.Configure(config =>
{
public static EnumSerializationMode EnumSerializationMode { get; set; }
= EnumSerializationMode.Strings;
}
config.EnumSerializationMode = EnumSerializationMode.Integers;
});
```

**Best Practice:**
Set during application startup before any database operations:
### Entity type mapping configuration

**Decision:** Provide a Fluent API to configure entity type mapping.

```csharp
// In Program.cs or Startup.cs
DbConnectionExtensions.EnumSerializationMode = EnumSerializationMode.Integers;
using static RentADeveloper.DbConnectionPlus.DbConnectionExtensions;

DbConnectionExtensions.Configure(config =>
{
config.Entity<Product>()
.ToTable("Products");

config.Entity<Product>()
.Property(a => a.Id)
.HasColumnName("ProductId");
.IsIdentity()
.IsKey();

config.Entity<Product>()
.Property(a => a.DiscountedPrice)
.IsComputed();

config.Entity<Product>()
.Property(a => a.IsOnSale)
.IsIgnored();
});
```

---
Expand All @@ -1036,7 +1100,10 @@ public class MyCustomDatabaseAdapter : IDatabaseAdapter
}

// Register adapter
DatabaseAdapterRegistry.RegisterAdapter<MyCustomConnection>(new MyCustomDatabaseAdapter());
DbConnectionExtensions.Configure(config =>
{
config.RegisterDatabaseAdapter<MyCustomConnection>(new MyCustomDatabaseAdapter());
});
```

**Use Cases:**
Expand Down
Loading