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
2 changes: 1 addition & 1 deletion .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
SW.EfCoreExtensions/SW.EfCoreExtensions.csproj
SW.EfCoreExtensions.PgSql/SW.EfCoreExtensions.PgSql.csproj
major-version: '10'
minor-version: '0'
minor-version: '1'
dotnet-version: '10.0.x'
run-tests: false
test-projects: '**/*UnitTests/*.csproj'
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Tests

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Unit tests
run: dotnet test SW.EfCoreExtensions.UnitTests
# Starts PostgreSQL in Docker through Testcontainers.
- name: PostgreSQL integration tests
run: dotnet test SW.EfCoreExtensions.PgSql.IntegrationTests
140 changes: 73 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,67 +1,73 @@

[![GitHub Actions](https://github.com/simplify9/EfCoreExtensions/actions/workflows/nuget-publish.yml/badge.svg)](https://github.com/simplify9/EfCoreExtensions/actions/workflows/nuget-publish.yml)
[![NuGet](https://img.shields.io/nuget/v/SimplyWorks.EfCoreExtensions?style=for-the-badge)](https://www.nuget.org/packages/SimplyWorks.EfCoreExtensions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE)

| **Package** | **Version** |
| :----------------:|:----------------------:|
|`SimplyWorks.EfCoreExtensions`| [![NuGet](https://img.shields.io/nuget/v/SimplyWorks.EfCoreExtensions?style=for-the-badge)](https://www.nuget.org/packages/SimplyWorks.EfCoreExtensions) |


## Overview

This repository contains open-source Entity Framework Core extensions for .NET, provided by Simplify9. The main NuGet package is [`SimplyWorks.EfCoreExtensions`](https://www.nuget.org/packages/SimplyWorks.EfCoreExtensions), targeting `net10.0` and licensed under MIT.

### Main Features
The extensions are organized by file and provide additional helpers for EF Core, including:

- **ChangeTrackerExtensions.cs**: Soft deletion, audit values, tenant values, domain event dispatching.
- **DbContextExtensions.cs**: Helpers for working with DbContext and relational types.
- **EntityEntryExtensions.cs**: Property setting utilities.
- **EntityTypeBuilderExtensions.cs**: Entity type builder helpers.
- **ExpressionExtensions.cs**: Expression utilities.
- **ICollectionExtensions.cs**: Collection update helpers.
- **IHostExtensions.cs**: Host-related helpers.
- **IQueryableOfTExtensions.cs**: Queryable extensions and search conditions.
- **ModelBuilderExtensions.cs**: Model builder helpers.
- **OwnedNavigationBuilderExtensions.cs**: Owned navigation builder helpers.

See the source files for the full list of available extension methods and their usage.

### Projects
- `SW.EfCoreExtensions`: Main EF Core extensions library.
- `SW.EfCoreExtensions.PgSql`: PostgreSQL-specific extensions.
- `SW.EfCoreExtensions.UnitTests`: Unit tests for the extensions.

### License
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.

### Build & CI
GitHub Actions are used for CI/CD. See the badge above for status. NuGet packages are published automatically on new releases.

>`Contact> BuildContact<TOwner>`

>`Dimensions> BuildDimensions<TOwner>`

>`Weight> BuildWeight<TOwner>`

>`Money> BuildMoney<TOwner>`

>`Audit> BuildAudit<TOwner>`

#### PropertyBuilderExtensions.cs

#### RelationalDbType.cs

#### [Searchy](https://github.com/simplify9/Searchy)
>`Search<TEntity>`

>`SearchyCondition`

>`SearchyFilter`

>`SearchySort`


## Getting support 👷
If you encounter any bugs, don't hesitate to submit an [issue](https://github.com/simplify9/EfCoreExtensions/issues). We'll get back to you promptly!

[![GitHub Actions](https://github.com/simplify9/EfCoreExtensions/actions/workflows/nuget-publish.yml/badge.svg)](https://github.com/simplify9/EfCoreExtensions/actions/workflows/nuget-publish.yml)
[![NuGet](https://img.shields.io/nuget/v/SimplyWorks.EfCoreExtensions?style=for-the-badge)](https://www.nuget.org/packages/SimplyWorks.EfCoreExtensions)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE)

| **Package** | **Version** |
| :----------------:|:----------------------:|
|`SimplyWorks.EfCoreExtensions`| [![NuGet](https://img.shields.io/nuget/v/SimplyWorks.EfCoreExtensions?style=for-the-badge)](https://www.nuget.org/packages/SimplyWorks.EfCoreExtensions) |


## Overview

This repository contains open-source Entity Framework Core extensions for .NET, provided by Simplify9. The main NuGet package is [`SimplyWorks.EfCoreExtensions`](https://www.nuget.org/packages/SimplyWorks.EfCoreExtensions), targeting `net10.0` and licensed under MIT.

### Main Features
The extensions are organized by file and provide additional helpers for EF Core, including:

- **ChangeTrackerExtensions.cs**: Soft deletion, audit values, tenant values, domain event dispatching.
- **DbContextExtensions.cs**: Helpers for working with DbContext and relational types.
- **EntityEntryExtensions.cs**: Property setting utilities.
- **EntityTypeBuilderExtensions.cs**: Entity type builder helpers.
- **ExpressionExtensions.cs**: Expression utilities.
- **ICollectionExtensions.cs**: Collection update helpers.
- **IHostExtensions.cs**: Host-related helpers.
- **IQueryableOfTExtensions.cs**: Queryable extensions and search conditions.
- **ModelBuilderExtensions.cs**: Model builder helpers.
- **OwnedNavigationBuilderExtensions.cs**: Owned navigation builder helpers.

See the source files for the full list of available extension methods and their usage.

### Projects
- `SW.EfCoreExtensions`: Main EF Core extensions library.
- `SW.EfCoreExtensions.PgSql`: PostgreSQL-specific extensions.
- `SW.EfCoreExtensions.UnitTests`: Unit tests for the extensions.

### License
This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.

### Build & CI
GitHub Actions are used for CI/CD. See the badge above for status. NuGet packages are published automatically on new releases.

>`Contact> BuildContact<TOwner>`

>`Dimensions> BuildDimensions<TOwner>`

>`Weight> BuildWeight<TOwner>`

>`Money> BuildMoney<TOwner>`

>`Audit> BuildAudit<TOwner>`

#### PropertyBuilderExtensions.cs

#### RelationalDbType.cs

#### [Searchy](https://github.com/simplify9/Searchy)
>`Search<TEntity>`

>`SearchyCondition`

>`SearchyFilter`

>`SearchySort`

Filter values are sent as SQL parameters, so a query shape compiles once and its plan is reused for every
value. Values convert to the field's type with the invariant culture, including `Guid`, enums (by name or
number) and booleans written as `1`/`0`. Sorts accept any property type and dotted paths
(`Customer.Address.City`). In `SimplyWorks.EfCoreExtensions.PgSql`, `StartsWith` and `Contains` are
case-insensitive `ILIKE`, with `%`, `_` and `\` in the value matched literally.


## Getting support 👷
If you encounter any bugs, don't hesitate to submit an [issue](https://github.com/simplify9/EfCoreExtensions/issues). We'll get back to you promptly!
138 changes: 138 additions & 0 deletions SW.EfCoreExtensions.PgSql.IntegrationTests/OrdersDatabase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Testcontainers.PostgreSql;

namespace SW.EfCoreExtensions.PgSql.IntegrationTests;

public enum OrderStatus { Draft = 0, Placed = 1, Shipped = 2, Cancelled = 3 }

public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Country { get; set; }
}

public class Order
{
public int Id { get; set; }
public string Number { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public OrderStatus Status { get; set; }
public decimal Total { get; set; }
public decimal? Discount { get; set; }
public bool Paid { get; set; }
public long Quantity { get; set; }
public Guid ExternalId { get; set; }
public DateTime PlacedOn { get; set; }
public List<OrderLine> Lines { get; set; } = new();
}

public class OrderLine
{
public int Id { get; set; }
public string Sku { get; set; }
public int Quantity { get; set; }
}

public class OrdersContext(DbContextOptions<OrdersContext> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
}

/// <summary>
/// One Postgres 17 container per test run with a small order book, plus what EF Core did: the SQL
/// it sent and how many times it compiled a query.
/// </summary>
[TestClass]
public static class OrdersDatabase
{
static PostgreSqlContainer container;
static DbContextOptions<OrdersContext> options;

static int compilations;
static readonly List<string> commands = new();

public static int Compilations => Volatile.Read(ref compilations);

public static IReadOnlyList<string> Commands
{
get { lock (commands) return commands.ToList(); }
}

public static void ClearCommands()
{
lock (commands) commands.Clear();
}

public static List<Order> All { get; private set; }

[AssemblyInitialize]
public static async Task Start(TestContext _)
{
container = new PostgreSqlBuilder().WithImage("postgres:17-alpine").Build();
await container.StartAsync();

// One options instance for the run, so every context shares EF Core's query cache, as the
// contexts of one application do.
options = new DbContextOptionsBuilder<OrdersContext>()
.UseNpgsql(container.GetConnectionString())
.LogTo(Record, new[] { CoreEventId.QueryCompilationStarting, RelationalEventId.CommandExecuted }, LogLevel.Debug)
.Options;

await using var db = NewContext();
await db.Database.EnsureCreatedAsync();

var customers = new[]
{
new Customer { Name = "Northwind", Country = "FR" },
new Customer { Name = "Contoso", Country = "DE" },
new Customer { Name = "Fabrikam", Country = "ES" },
};
db.Customers.AddRange(customers);

var start = new DateTime(2026, 10, 1, 0, 0, 0, DateTimeKind.Utc);
for (var i = 1; i <= 30; i++)
db.Orders.Add(new Order
{
Number = i == 7 ? "PROMO_50%" : $"ORD-{i:000}",
Customer = customers[i % 3],
Status = (OrderStatus)(i % 4),
Total = 10m * i + 0.5m,
Discount = i % 5 == 0 ? null : i % 7,
Paid = i % 2 == 0,
Quantity = 1_000_000_000L * (i % 6),
ExternalId = new Guid($"00000000-0000-0000-0000-{i:000000000000}"),
PlacedOn = start.AddHours(13 * i),
Lines = { new OrderLine { Sku = $"SKU-{i % 4}", Quantity = i } },
});
await db.SaveChangesAsync();

All = await db.Orders.AsNoTracking().Include(o => o.Customer).Include(o => o.Lines).ToListAsync();
}

[AssemblyCleanup]
public static async Task Stop()
{
if (container != null) await container.DisposeAsync();
}

public static OrdersContext NewContext() => new(options);

static void Record(string message)
{
if (message.Contains(nameof(CoreEventId.QueryCompilationStarting)))
Interlocked.Increment(ref compilations);
else
lock (commands) commands.Add(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.7.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.7.1" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.10.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\SW.EfCoreExtensions.PgSql\SW.EfCoreExtensions.PgSql.csproj" />
</ItemGroup>

</Project>
Loading
Loading