diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5f778c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build ${{ matrix.project }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + project: + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Build + run: dotnet build ${{ matrix.project }} --configuration Release -p:ContinuousIntegrationBuild=true + + test: + name: Test ${{ matrix.project }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + project: + - tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Test + run: dotnet test ${{ matrix.project }} --configuration Release diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml deleted file mode 100644 index 372c6ff..0000000 --- a/.github/workflows/dotnetcore.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: eQuantic Core Data EntityFramework - -on: [push] - -jobs: - build: - runs-on: windows-latest - - steps: - - uses: actions/checkout@v3 - - name: Setup .NET Core - uses: actions/setup-dotnet@v3 - with: - dotnet-version: 10.0.x - - name: Build eQuantic.Core.Data.EntityFramework Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 6 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 7 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 6 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 7 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.SqlServer .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.MySql Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MySql .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MySql .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MySql .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql .net 8 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.PostgreSql .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj --configuration Release - - - name: Build eQuantic.Core.Data.EntityFramework.MongoDb Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MongoDb .net 9 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj --configuration Release - - name: Build eQuantic.Core.Data.EntityFramework.MongoDb .net 10 Library - run: dotnet build ./src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj --configuration Release - - - name: Push package into Nuget.org - run: dotnet nuget push **/*.nupkg --skip-duplicate -k ${{secrets.nuget_key}} -s https://api.nuget.org/v3/index.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cc821e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,111 @@ +name: Release + +# Only a pushed version tag (e.g. v10.0.3) triggers a release. A plain push to any branch — including +# master — no longer publishes anything; that was the previous, unintentional behavior (see +# docs/IMPROVEMENT_PLAN.md, finding Q1). To release: `git tag vX.Y.Z && git push origin vX.Y.Z`. +on: + push: + tags: + - 'v*.*.*' + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + name: Build ${{ matrix.project }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + project: + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj + - src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Build & pack + run: dotnet build ${{ matrix.project }} --configuration Release -p:ContinuousIntegrationBuild=true + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: nupkg-${{ strategy.job-index }} + path: artifacts/*.nupkg + if-no-files-found: error + retention-days: 7 + + test: + name: Test ${{ matrix.project }} + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + project: + - tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj + - tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget- + - name: Test + run: dotnet test ${{ matrix.project }} --configuration Release + + publish: + name: Publish to NuGet.org + runs-on: ubuntu-latest + permissions: + contents: read + needs: [build, test] + # This environment gates the publish behind whatever protection rules are configured for it in + # the repo's Settings -> Environments (e.g. required reviewers). GitHub auto-creates an environment + # on first use with NO protection rules, so a repo admin must add them for the gate to be effective. + environment: + name: nuget-release + url: https://www.nuget.org/profiles/eQuantic + steps: + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Download all package artifacts + uses: actions/download-artifact@v4 + with: + pattern: nupkg-* + path: artifacts + merge-multiple: true + - name: Push to NuGet.org + env: + NUGET_KEY: ${{ secrets.nuget_key }} + run: dotnet nuget push "artifacts/*.nupkg" --skip-duplicate -k "$NUGET_KEY" -s https://api.nuget.org/v3/index.json diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..58d0fb3 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,32 @@ + + + + + + eQuantic Systems + Copyright © 2016 + https://github.com/eQuantic/core-data-entityframework + https://github.com/eQuantic/core-data-entityframework + Git + LICENSE + README.md + Icon.png + latest + $(MSBuildThisFileDirectory)artifacts/ + true + false + false + false + + + + + + + + + diff --git a/README.md b/README.md index 44faac1..690b10e 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,107 @@ -# eQuantic Core Data Entity Framework Library +# eQuantic.Core.Data.EntityFramework -The **eQuantic Data Core** provides a robust implementation of the **Repository Pattern** specifically for **Entity Framework Core**. +**The Entity Framework Core implementation of [eQuantic.Core.Data](https://github.com/eQuantic/core-data).** +You code against the provider-agnostic `IRepository` / `IUnitOfWork` contracts; this package +supplies the EF Core engine for **SQL Server, PostgreSQL, MySQL and MongoDB**. -This library offers seamless integration with the following database providers: +```csharp +// A repository over any IEntity, obtained from your DbContext-backed unit of work: +var repo = unitOfWork.GetAsyncRepository(); -- **SQL Server** -- **PostgreSQL** -- **MySQL** -- **MongoDB** (via EF Core provider) +// Query typed and fluent — one QueryOptions, no magic strings: +var page = await repo.GetPagedAsync( + PageRequest.Of(pageIndex: 1, pageSize: 20), + new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThan, 100m) + .And(o => o.Customer.Name, FilterOperator.Contains, term) + .OrderByDescending(o => o.CreatedAt) + .Include(nameof(OrderData.Customer)) + .NoTracking()); -## Version 4.4.0 +// page is a PagedResult: Items + TotalCount + PageIndex/PageSize/PageCount + Has*Page +``` + +## What this package gives you -### Key Features and Improvements (v4.4.0) +`eQuantic.Core.Data` defines the **contracts** — `IRepository`, `IUnitOfWork`, `QueryOptions`, +`PageRequest`, `PagedResult`, specifications — with the persistence engine kept out of the type signatures +(`IRepository`, not `IRepository`). -- **.NET 10 Support**: Full compatibility with .NET 10, including optimized `ExecuteUpdate` operations using the new `UpdateSettersBuilder`. -- **Improved Expression Conversion**: Enhanced reflection-based method lookup for `ExecuteUpdate` setters, ensuring robustness across different .NET frameworks and provider-specific quirks. -- **Optimized Resource Management**: Implemented internal cleanup mechanisms in `UnitOfWork` to better manage memory and database connections. -- **Enhanced Data Integrity**: Fixed shadow field inheritance issues by replacing brittle `new` keyword usage with `internal virtual` properties. -- **Strict Pagination Validation**: Added explicit validation for pagination parameters in `QueryableReadRepository`. -- **Full Multi-Provider Support**: Optimized implementations for **SqlServer**, **PostgreSql**, **MySql**, and **MongoDb**. +This package is the **Entity Framework Core implementation** of those contracts. It translates a single +`QueryOptions` into an EF `IQueryable` — applying, in order, custom *before* hooks, the +specification and predicate filter, eager `Include`s, sortings (server-side / EF-translatable), +`AsNoTracking`, `IgnoreQueryFilters`, query tags and custom *after* hooks — and returns `PagedResult` +from paged reads. The relational providers also carry a parameterized raw-SQL executor (functions and +stored procedures, always via `DbParameter`). -## Installation +## How you query -To install **eQuantic.Core.Data.EntityFramework**, run the following command in the [Package Manager Console](https://docs.nuget.org/docs/start-here/using-the-package-manager-console): +`QueryOptions` mirrors the eQuantic.Linq query builders, so filters read like code and fail at +compile time — not at runtime: -```powershell -Install-Package eQuantic.Core.Data.EntityFramework +```csharp +new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThanOrEqual, 100m) // typed member selector + .And(o => o.Status, FilterOperator.Equal, OrderStatus.Paid) // clauses fold left to right: + .Or(o => o.Customer.IsVip, FilterOperator.Equal, true) // (total>=100 AND paid) OR vip + .OrderByDescending(o => o.CreatedAt) + .ThenBy("customer.name") // string path for dynamic columns + .Include(nameof(OrderData.Customer)) + .NoTracking(); ``` -For specific providers, install the corresponding package: +You reach for whichever filter form fits — member selector, `string` path, `ISpecification`, +`Expression>`, a serialized `ExpressionModel`, or an `eQuantic.Linq.Web` query string — all +end up as one predicate the provider translates. The full query-string grammar is documented in the +[eQuantic.Linq reference](https://github.com/eQuantic/core-linq/blob/main/docs/query-string-syntax.md). + +## Providers + +| Package | Database | +|---|---| +| `eQuantic.Core.Data.EntityFramework.SqlServer` | SQL Server | +| `eQuantic.Core.Data.EntityFramework.PostgreSql` | PostgreSQL | +| `eQuantic.Core.Data.EntityFramework.MySql` | MySQL (Pomelo) | +| `eQuantic.Core.Data.EntityFramework.MongoDb` | MongoDB (EF Core provider) | + +The three relational providers share `eQuantic.Core.Data.EntityFramework.Relational`; every provider builds +on the base `eQuantic.Core.Data.EntityFramework`. Register your `DbContext`-backed unit of work and the +open-generic repositories through `AddRelationalRepositories()` — the +full wiring is in the [walkthrough](Repository.md). + +## Versioning — pick the package major that matches your runtime + +This library targets **.NET 8** and **.NET 10**, and each runtime is published as its **own package major** +so the EF Core lines never mix: + +| Your app | Install | Targets | +|---|---|---| +| .NET 8 | **8.x** (e.g. `8.2.0`) | `net8.0`, EF Core 8 | +| .NET 10 | **10.x** (e.g. `10.1.0`) | `net10.0`, EF Core 10 | + +> The shared multi-framework assemblies (the referenceable base `eQuantic.Core.Data.EntityFramework` and +> `eQuantic.Core.Data.EntityFramework.Relational`) stay in the **4.x** line on purpose — a neutral lane that +> must not be read as a .NET version. You normally consume only the provider package for your runtime +> (8.x / 10.x), which pulls the right shared assemblies transitively. + +## Install + +```bash +# .NET 8 app + SQL Server +dotnet add package eQuantic.Core.Data.EntityFramework.SqlServer --version 8.* + +# .NET 10 app + PostgreSQL +dotnet add package eQuantic.Core.Data.EntityFramework.PostgreSql --version 10.* +``` -- `eQuantic.Core.Data.EntityFramework.SqlServer` -- `eQuantic.Core.Data.EntityFramework.PostgreSql` -- `eQuantic.Core.Data.EntityFramework.MySql` -- `eQuantic.Core.Data.EntityFramework.MongoDb` +Swap the suffix for `PostgreSql`, `MySql` or `MongoDb` as needed. -## Usage Examples +## Learn more -The following are examples of implementing the repository pattern: +- [Repository Pattern walkthrough](Repository.md) — data entities, unit of work, repository and + specifications, end to end. +- [eQuantic.Core.Data](https://github.com/eQuantic/core-data) — the contracts and the `QueryOptions` / + `PagedResult` / `PageRequest` query surface, backed by the + [eQuantic.Linq](https://github.com/eQuantic/core-linq) query engine. -- [Repository Pattern Implementation](Repository.md) +MIT © eQuantic Tech diff --git a/Repository.md b/Repository.md index 440a036..93c91b4 100644 --- a/Repository.md +++ b/Repository.md @@ -1,263 +1,336 @@ -# Repository Pattern with Entity Framework +# Repository walkthrough (Entity Framework Core) -## UnitOfWork example: +An end-to-end slice built on **eQuantic.Core.Data v5** and this EF Core provider: data entities → a +unit of work → repositories → specifications → a domain service that consumes them. You code against the +provider-agnostic contracts (`IEntity`, `IQueryableUnitOfWork`, `QueryOptions`, +`PageRequest`/`PagedResult`); the provider supplies the EF Core engine. The examples use SQL Server — +PostgreSQL and MySQL are identical (swap the provider namespace and `UseSqlServer`), and MongoDB differs +only in registration (see §3). + +## 1. Data entities — `IEntity` + +An entity is a plain class that implements `IEntity`. The key is exposed through `GetKey()`/ +`SetKey()` (there is no mandated `Id` property, though you will usually have one). + +```csharp +using System; +using eQuantic.Core.Data.Repository; + +public enum OrderStatus { Pending, Paid, Cancelled } + +public class CustomerData : IEntity +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public bool IsVip { get; set; } + + public Guid GetKey() => Id; + public void SetKey(Guid key) => Id = key; +} + +public class OrderData : IEntity +{ + public Guid Id { get; set; } + public decimal Total { get; set; } + public OrderStatus Status { get; set; } + public DateTime CreatedAt { get; set; } + + public Guid CustomerId { get; set; } + public CustomerData Customer { get; set; } = default!; + + public Guid GetKey() => Id; + public void SetKey(Guid key) => Id = key; +} +``` + +## 2. The DbContext + +A regular EF Core `DbContext` — the provider works with whatever context you already have. ```csharp -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Ioc; using Microsoft.EntityFrameworkCore; -namespace eQuantic.Core.Web.Examples.Infrastructure +public class AppDbContext(DbContextOptions options) : DbContext(options) { - public class ExampleUnitOfWork : UnitOfWork - { - private readonly IContainer _container; - - public ExampleUnitOfWork(IContainer container, DbContext context) : base(context) - { - _container = container; - } - - public override TRepository GetRepository() - { - return _container.Resolve(); - } - - public override TRepository GetRepository(string name) - { - return _container.Resolve(name); - } - } + public DbSet Orders => Set(); + public DbSet Customers => Set(); } ``` -## Entity data example: +## 3. The unit of work + +Derive the provider's `UnitOfWork`. Declaring your own interface (deriving +`IQueryableUnitOfWork`) keeps call sites decoupled from the concrete type. ```csharp using System; +using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; // provider base using eQuantic.Core.Data.Repository; -namespace eQuantic.Core.Web.Examples.Infrastructure.Data +public interface IAppUnitOfWork : IQueryableUnitOfWork { - public class UserData : IEntity - { - public Guid Id { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public string Email { get; set; } - } } -namespace eQuantic.Core.Web.Examples.Infrastructure.Data +public class AppUnitOfWork : UnitOfWork, IAppUnitOfWork { - public class PersonData : IEntity + public AppUnitOfWork(IServiceProvider serviceProvider, AppDbContext context) + : base(serviceProvider, context) { - public Guid Id { get; set; } - public string Name { get; set; } - public DateTime BirthDate { get; set; } - public string Phone { get; set; } - public virtual UserData User { get; set; } } } ``` -## Repository example: +Register the context and the repositories. `AddRelationalRepositories` wires the unit of work, the generic +repositories, and — because the relational unit of work implements it — the raw-SQL +`ISqlUnitOfWork`. -### Contract +```csharp +using eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +services.AddDbContext(o => o.UseSqlServer(connectionString)); +services.AddRelationalRepositories(); +``` + +No custom unit-of-work type? Use the provider's `DefaultUnitOfWork` (over a bare `DbContext`) instead. +MongoDB has no SQL executor, so its unit of work derives +`eQuantic.Core.Data.EntityFramework.MongoDb.Repository.UnitOfWork` and is registered with the +SQL-agnostic `AddQueryableRepositories()`. + +## 4. Getting a repository + +Ask the unit of work for a repository over any `IEntity`. The queryable accessors resolve the +generic repositories registered above: + +```csharp +// asynchronous read + write: +IAsyncRepository orders = + unitOfWork.GetAsyncRepository(); + +// synchronous sibling: +IRepository ordersSync = + unitOfWork.GetRepository(); +``` + +All four accessors resolve from the generic registration above — the plain +`GetAsyncRepository`/`GetRepository` shown here and the richer +`GetAsyncQueryableRepository`/`GetQueryableRepository` variants (which add the `IQueryable`/`ISet` surface). +Custom repositories (§8) layer your own named interface on top of the same wiring. + +## 5. Reading — one `QueryOptions` + +All query shaping — filtering, includes, sorting, tracking — is expressed through a single +`QueryOptions`. Filters read like code and fail at compile time; clauses fold left to right. + +```csharp +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Web; // FilterOperator + +var options = new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThanOrEqual, 100m) // typed member selector + .And(o => o.Status, FilterOperator.Equal, OrderStatus.Paid) // (total>=100 AND paid) + .Or(o => o.Customer.IsVip, FilterOperator.Equal, true) // OR the customer is VIP + .OrderByDescending(o => o.CreatedAt) + .ThenBy("customer.name") // string path for dynamic columns + .Include(nameof(OrderData.Customer)) // eager load + .NoTracking(); +``` + +Paged reads return a `PagedResult` — the items plus the totals needed to render a pager: ```csharp -using System; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -namespace eQuantic.Core.Web.Examples.Infrastructure.Repositories.Contracts -{ - public interface IPersonRepository : IAsyncRepository - { - } -} +PagedResult page = await orders.GetPagedAsync(PageRequest.Of(pageIndex: 1, pageSize: 20), options); + +foreach (var order in page.Items) { /* ... */ } +_ = page.TotalCount; // total across all pages +_ = page.PageIndex; _ = page.PageSize; // echoed back +_ = page.PageCount; // computed +_ = page.HasPreviousPage; _ = page.HasNextPage; ``` -### Implementation +The rest of the read surface follows the same shape — pass a `QueryOptions` (or none): ```csharp -using System; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -using eQuantic.Core.Web.Examples.Infrastructure.Repositories.Contracts; +// by key (a non-null options routes through the key predicate + includes; otherwise DbSet.Find): +OrderData? one = await orders.GetAsync(id, new QueryOptions().Include(nameof(OrderData.Customer))); -namespace eQuantic.Core.Web.Examples.Infrastructure.Repositories -{ - public class PersonRepository : AsyncRepository, IPersonRepository - { - public PersonRepository(ExampleUnitOfWork unitOfWork) : base(unitOfWork) - { - } - } -} +// by predicate: +IEnumerable big = await orders.GetFilteredAsync(o => o.Total >= 100m, new QueryOptions().NoTracking()); + +// projection (server-side Select): +IEnumerable summaries = await orders.GetMappedAsync( + o => new OrderSummary(o.Id, o.Total, o.Customer.Name), + new QueryOptions().Include(nameof(OrderData.Customer))); + +// paged projection: +PagedResult summaryPage = await orders.GetPagedAsync( + PageRequest.Of(1, 20), + o => new OrderSummary(o.Id, o.Total, o.Customer.Name), + new QueryOptions().Include(nameof(OrderData.Customer))); + +// aggregates (Count returns long; Sum has an overload per numeric type): +long paidCount = await orders.CountAsync(new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid)); +decimal paidTotal = await orders.SumAsync(o => o.Total, new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid)); + +public record OrderSummary(Guid Id, decimal Total, string Customer); ``` -# DDD Pattern +Every method has a synchronous twin on the queryable repository (`GetPaged`, `Get`, `Count`, `Sum`, …). -## Domain Entity example: +## 6. Specifications + +Encapsulate a reusable rule as an `ISpecification` (base class `Specification`). ```csharp using System; +using System.Linq.Expressions; +using eQuantic.Linq.Specification; -namespace eQuantic.Core.Web.Examples.Domain.Entities +public sealed class PaidOrdersSpecification : Specification { - public class User - { - public ShortGuid Id { get; set; } - public string UserName { get; set; } - public string Password { get; set; } - public string Email { get; set; } - } + public override Expression> SatisfiedBy() => o => o.Status == OrderStatus.Paid; } +``` -namespace eQuantic.Core.Web.Examples.Domain.Entities -{ - public class Person - { - public ShortGuid Id { get; set; } - public string Name { get; set; } - public DateTime BirthDate { get; set; } - public string Phone { get; set; } - public virtual User User { get; set; } - } -} +Apply it either as the filter inside `QueryOptions`, or directly through `AllMatchingAsync`: + +```csharp +var spec = new PaidOrdersSpecification(); + +// inside QueryOptions (composes with sorting, includes, tracking, ...): +var recentPaid = await orders.GetPagedAsync( + PageRequest.Of(1, 20), + new QueryOptions().Where(spec).OrderByDescending(o => o.CreatedAt)); + +// or directly: +IEnumerable allPaid = await orders.AllMatchingAsync(spec, new QueryOptions().NoTracking()); ``` -## Specification Pattern +## 7. Writing + +Writes are staged on the repository and persisted when the unit of work commits. ```csharp -using System; -using System.Linq.Expressions; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -using eQuantic.Core.Linq.Specification; +var order = new OrderData { Id = Guid.NewGuid(), Total = 150m, Status = OrderStatus.Pending, CreatedAt = DateTime.UtcNow }; -namespace eQuantic.Core.Web.Examples.Domain.Specification -{ - public class PersonSpecification : Specification - { - private readonly string _term; - - public PersonSpecification(string term) - { - _term = term; - } - public override Expression> SatisfiedBy() - { - return p => p.Name.StartsWith(_term) || p.User.UserName.StartsWith(_term) || p.User.Email.StartsWith(_term); - } - } -} +await orders.AddAsync(order); // stage an insert +await orders.AddRangeAsync(new[] { order1, order2 }); // stage several + +order.Status = OrderStatus.Paid; +await orders.ModifyAsync(order); // mark modified + +await orders.RemoveAsync(order); // stage a delete + +int affected = await unitOfWork.CommitAsync(); // one round-trip, returns affected rows ``` -## Domain Services +Set-based writes run directly in the database (EF `ExecuteUpdate`/`ExecuteDelete`) and do not need a +commit: + +```csharp +long cancelled = await orders.UpdateManyAsync( + o => o.Status == OrderStatus.Pending, + o => new OrderData { Status = OrderStatus.Cancelled }); + +long removed = await orders.DeleteManyAsync(o => o.Total == 0m); +``` -### Contract +`DeleteManyAsync`/`UpdateManyAsync` also accept an `ISpecification` in place of the predicate. + +## 8. Custom repositories + +Need repository-specific methods, or the plain `IRepository`/`IAsyncRepository` shape resolved by +`GetRepository`/`GetAsyncRepository`? Declare an interface and derive the generic base: ```csharp -using System; -using eQuantic.Core.Collections; -using eQuantic.Core.Web.Examples.Domain.Entities; +using eQuantic.Core.Data.EntityFramework.Repository; +using eQuantic.Core.Data.Repository; + +public interface IOrderRepository : IRepository, IAsyncRepository +{ +} -namespace eQuantic.Core.Web.Examples.Domain.Services.Contracts +public class OrderRepository : AsyncQueryableRepository, IOrderRepository { - public interface IPersonService + public OrderRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { - Person Get(Guid id); - bool Create(Person person); - bool Update(Person person); - bool Delete(Guid id); - PagedList Find(string term, int pageIndex, int pageSize); } } ``` -### Implementation +Register by scanning the assembly, then resolve through the unit of work (or inject `IOrderRepository` +directly): + +```csharp +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; + +services.AddDbContext(o => o.UseSqlServer(connectionString)); +services.AddCustomRepositories(o => o + .AddLifetime(ServiceLifetime.Scoped) + .FromAssembly(typeof(OrderRepository).Assembly)); + +// later: +IAsyncRepository repo = unitOfWork.GetAsyncRepository(); +IRepository repoSync = unitOfWork.GetRepository(); +``` + +## 9. A domain service + +Putting it together — a service consumes the unit of work and its repositories, keeping EF Core out of the +domain layer. ```csharp using System; -using System.Collections.Generic; -using AutoMapper; -using eQuantic.Core.Collections; -using eQuantic.Core.Linq; -using eQuantic.Core.Web.Examples.Domain.Entities; -using eQuantic.Core.Web.Examples.Domain.Specification; -using eQuantic.Core.Web.Examples.Infrastructure; -using eQuantic.Core.Web.Examples.Infrastructure.Data; -using eQuantic.Core.Web.Examples.Infrastructure.Repositories.Contracts; - -namespace eQuantic.Core.Web.Examples.Domain.Services +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Web; + +public class OrderService { - public class PersonService : IPersonService - { - public IMapper Mapper { get; } - public ExampleUnitOfWork UnitOfWork { get; } - - public PersonService(IMapper mapper, ExampleUnitOfWork unitOfWork) - { - Mapper = mapper; - UnitOfWork = unitOfWork; - } - - public Person Get(Guid id) - { - Person person = null; - var repo = UnitOfWork.GetRepository(); - var item = repo.Get(id, p => p.User); - if (item != null) person = Mapper.Map(item); - - return person; - } - - public bool Create(Person person) - { - var repo = UnitOfWork.GetRepository(); - var item = Mapper.Map(person); - repo.Add(item); - return UnitOfWork.Commit() > 0; - } - - public bool Update(Person person) - { - var repo = UnitOfWork.GetRepository(); - var item = repo.Get(person.Id); - Mapper.Map(person, item); - repo.Modify(item); - return UnitOfWork.Commit() > 0; - } - - public bool Delete(Guid id) - { - var repo = UnitOfWork.GetRepository(); - var item = repo.Get(id); - repo.Remove(item); - return UnitOfWork.Commit() > 0; - } - - public PagedList Find(string term, int pageIndex, int pageSize) - { - var repo = UnitOfWork.GetRepository(); - var specification = new PersonSpecification(term); - var count = repo.Count(specification); - var items = repo.GetPaged(specification, pageIndex, pageSize, - new[] {new Sorting {Column = c => c.Name}}, p => p.User); - var persons = Mapper.Map>(items); - return new PagedList(persons, count){ PageIndex = pageIndex, PageSize = pageSize}; - } - } -} -``` + private readonly IAppUnitOfWork _unitOfWork; -## Advanced Features (v4.4.0) + public OrderService(IAppUnitOfWork unitOfWork) => _unitOfWork = unitOfWork; -### .NET 10 ExecuteUpdate Support + public Task GetAsync(Guid id, CancellationToken ct = default) => + _unitOfWork.GetAsyncRepository() + .GetAsync(id, new QueryOptions().Include(nameof(OrderData.Customer)), ct); -Starting with version 4.4.0, the repository fully supports .NET 10's `ExecuteUpdate` with the new `UpdateSettersBuilder` syntax. The `ExpressionConverter` automatically handles the transformation of anonymous object or member init expressions into the optimized EF Core format. + public async Task CreateAsync(OrderData order, CancellationToken ct = default) + { + var repo = _unitOfWork.GetAsyncRepository(); + await repo.AddAsync(order, ct); + await _unitOfWork.CommitAsync(ct); + return order.Id; + } -### Resource Management + public async Task DeleteAsync(Guid id, CancellationToken ct = default) + { + var repo = _unitOfWork.GetAsyncRepository(); + var order = await repo.GetAsync(id, cancellationToken: ct); + if (order is null) return false; -The `UnitOfWork` implementation now includes internal resource tracking to ensure that database connections and related resources are properly disposed of, even when multiple repositories are shared within the same scope. + await repo.RemoveAsync(order); + return await _unitOfWork.CommitAsync(ct) > 0; + } + + public Task> FindAsync(decimal minTotal, int pageIndex, int pageSize, CancellationToken ct = default) => + _unitOfWork.GetAsyncRepository() + .GetPagedAsync( + PageRequest.Of(pageIndex, pageSize), + new QueryOptions() + .Where(o => o.Total, FilterOperator.GreaterThanOrEqual, minTotal) + .OrderByDescending(o => o.CreatedAt) + .Include(nameof(OrderData.Customer)) + .NoTracking(), + ct); + + public Task RevenueAsync(CancellationToken ct = default) => + _unitOfWork.GetAsyncRepository() + .SumAsync(o => o.Total, new QueryOptions().Where(o => o.Status, FilterOperator.Equal, OrderStatus.Paid), ct); +} +``` diff --git a/build/Directory.Build.targets b/build/Directory.Build.targets deleted file mode 100644 index 486eff8..0000000 --- a/build/Directory.Build.targets +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/build/MSBump.props b/build/MSBump.props deleted file mode 100644 index 21486c9..0000000 --- a/build/MSBump.props +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/build/MSBump.targets b/build/MSBump.targets deleted file mode 100644 index 66d54cc..0000000 --- a/build/MSBump.targets +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - $(MSBumpNewVersion) - $(MSBumpNewVersion) - - - - - - - - - True - - - \ No newline at end of file diff --git a/docs/IMPROVEMENT_PLAN.md b/docs/IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..9350575 --- /dev/null +++ b/docs/IMPROVEMENT_PLAN.md @@ -0,0 +1,331 @@ +# Improvement Plan — eQuantic.Core.Data.EntityFramework + +> Deep analysis performed on 2026-07-16 of this repository (v4.4.2 / published 6.x–10.x lines) and of the +> contracts repository [`eQuantic/core-data`](https://github.com/eQuantic/core-data) (v4.3.2). +> Every finding cites `file:line` and was verified against the source code, not inferred. + +## Executive summary + +The package works and has been published on nuget.org for years (106 versions of the main package, 57 of +the contract), but it accumulated debt across five layers. In order of severity: + +1. **Security (critical):** `SqlExecutor` builds SQL by interpolating values without escaping — a real SQL + injection in `ExecuteFunction`/`ExecuteProcedure` across the SqlServer, PostgreSql and MySql providers. + In addition, CI publishes to nuget.org on every push to any branch. +2. **Correctness (critical/high):** double-dispose of the `UnitOfWork`, the MongoDb provider operating + against the wrong database (silently writing/deleting), `EXEC` (T-SQL) copied to PostgreSQL/MySQL, + parameters lost in `FromSqlRaw`, `configuration` discarded in `All`/`Any`, a DI registration that throws + at runtime for MongoDb, and bulk updates that silently write the wrong values. +3. **Packaging/versioning (critical for consumers):** the same `PackageId` is published as parallel version + lines (4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target). NuGet treats it all as a single timeline: + "latest" = 10.0.2 (net10-only), breaking restore for anyone on net6–net9 and making the 4.x line (the + most complete) look abandoned. There are 21 hand-maintained csproj files, and the scheme already produced + real dependency-graph bugs. +4. **Structural duplication:** ~2,400 lines are identical copies between providers — PostgreSql and MySql + are 100% renamed copies of SqlServer. That is the root cause of the `EXEC`→`CALL` bug (copied without + adapting the dialect). +5. **Contracts (`eQuantic.Core.Data`):** combinatorial interface explosion + (`IAsyncReadRepository` has **100 members**; `SumAsync` alone is 30 overloads), + NRT disabled, and pagination returning `IEnumerable` with no total count. + +The plan below is organized into **5 phases**. Phases 0–2 are **non contract-breaking** and can ship on the +current line. Phases 3–4 define the **v5.0.0 of the contracts** (deliberately breaking) and the versioning +strategy on nuget.org. + +## Implementation status (Phase 1 — done) + +Fixes already applied on this branch, **with tests** (28 tests passing; all packages build — +base/Relational/SqlServer/PostgreSql/MySql on net10, MongoDb on net8): + +| Finding | Fix | Commit | +|--------|----------|--------| +| **S1** SQL injection + **P4** + **P3** | Parameterized `SqlExecutor` (placeholders instead of interpolating values) across the 3 SQL providers; `EXEC`→`CALL` on PG/MySql; `.ToArray()` in `ExecuteQuery`. Tests prove the malicious value never reaches the SQL. | `fix(sql)` | +| **C3** DI crash | `ISqlUnitOfWork` registered only when the implementation provides it; removed the duplicate `IQueryableUnitOfWork` registration. | `fix(core)` | +| **C1** double-dispose | Unified `_disposed` flag (`protected`); the UoW is disposed exactly once. | `fix(core)` | +| **A1** configuration discarded | Sync `All`/`Any` forward `configuration`. | `fix(read)` | +| **A2** default key | `Get`/`GetAsync` validate `id is null` instead of `default(TKey)`. | `fix(read)` | +| **P1/P6** MongoDb | Uses the database configured on the `DbContext` (not the collection name); throws instead of returning a silent `0`. | `fix(mongodb)` | +| **A4/M7** key | Key value parameterized (closure) instead of a literal; PK metadata cached; `EF.Property` for shadow keys; throws on a partial composite key. | `fix(core)` | +| **M4** DI | `AddRepository` honours the configured lifetime, uses `TryAdd`, and tolerates `ReflectionTypeLoadException`. | `fix(di)` | +| **M2** async | `ConfigureAwait(false)` across 91 library awaits (base + 4 providers). | `perf(async)` | +| **P2** MongoDb | `UpdateDefinitionBuilder` rejects (throws on) update expressions that reference the entity instead of silently writing a constant. New MongoDb test project. | `fix(mongodb)` | +| **C2** ownership ⚠️ | The repository no longer disposes the injected `UnitOfWork` (the creator — DI container or caller — owns the lifetime). **Behavioural change.** | `fix(core)` | +| **A5** pagination | `GetPaged`/`GetPagedAsync` order by the primary key when no explicit ordering is given (deterministic pagination); the caller's ordering is preserved. | `fix(read)` | + +The placebo test (`Assert.Pass()`) was replaced with real coverage: DI, disposal, SQL parameterization, +queries, the key expression, pagination and the MongoDb `UpdateDefinitionBuilder`, via EF InMemory. + +⚠️ **C2 is the only behavioural change in this batch.** Anyone who relied on disposing the repository to +close a manually-created context must now dispose the `UnitOfWork`/`DbContext` directly (the DI container +already does this). Documented in the commit. + +**Investigated and corrected during diagnosis:** plan item **M8** (remove `Where(_ => true)` in +`GetAllAsync`) was **wrong** — that `Where` is load-bearing (`SetBase` does not implement +`IAsyncEnumerable`; the `Where` turns it into a real EF `IQueryable`). Documented in code + a regression +test; nothing removed. + +Note on **P2**: the current fix **rejects** update expressions that reference the entity (avoids silent +corruption). Actually supporting them via `$inc`/pipeline updates is left for a future MongoDb iteration. + +## Implementation status (Phase 0 — done, reduced scope) + +| Finding | Fix | +|--------|----------| +| **PK7** dead MSBump | Removed `build/MSBump.props` (circular self-import), `build/MSBump.targets` (missing task) and `build/Directory.Build.targets` (outside any ancestor chain — never imported). Nothing referenced them; verified by grep before deleting. | +| **PK2** graph bug | `MySql.Net10.csproj` referenced the **Net9** base instead of **Net10** — fixed. | +| **Q1** accidental publishing | CI split into **`ci.yml`** (build + test on every push/PR, publishing nothing) and **`release.yml`** (only on a `vX.Y.Z` tag, publishing behind a `nuget-release` GitHub Environment). | +| **Q2** no tests in CI | `ci.yml` runs `dotnet test` on the 3 test projects (previously none ran). | +| **Q3** dated pipeline | Actions updated (`checkout@v4`, `setup-dotnet@v4`), `ubuntu-latest` instead of `windows-latest`, NuGet cache, `-p:ContinuousIntegrationBuild=true`. | +| **Q4** unpinned SDK | `global.json` at the root pinning `10.0.100` with `rollForward: latestFeature`. | + +**Design decision (matrix build, not `dotnet build sln`):** I tried running +`dotnet build eQuantic.Core.Data.EntityFramework.sln` locally to simplify the 23 build steps — and +reproduced exactly the **PK4** finding: because several `.csproj` files of the same package +(`eQuantic.Core.Data.EntityFramework.csproj`, `.Net6.csproj`, `.Net7.csproj`, …) share the same folder +without their own `BaseIntermediateOutputPath`, the parallel solution build corrupted each other's +`project.assets.json` and hit a file-in-use `IOException`. The two new workflows keep the builds +**individual** (as the old workflow already did), but each one runs as a **matrix job** — i.e. an isolated +runner/checkout — which eliminates the shared `obj`/`bin` without having to settle the versioning decision +(Part IV) first. + +⚠️ **Two things only a maintainer with GitHub access can finish:** +1. **Nothing publishes automatically until a tag exists.** Before, any push to any branch attempted to + publish (mitigated only by `--skip-duplicate`). Now you need `git tag vX.Y.Z && git push origin vX.Y.Z` + to trigger `release.yml`. This is intentional (finding Q1) but changes the workflow. +2. **The `environment: nuget-release` referenced by `release.yml` has no protection until configured.** + GitHub auto-creates the Environment on first use with no required reviewers or branch/tag restriction — + this session's PR tool has no permission to configure it. In *Settings → Environments → nuget-release*, + add at least one required reviewer for the gate to actually work. + +**Deliberately out of scope for Phase 0** (not done, because it depends on the still-open versioning +decision — Part IV): adopting MinVer (would change how each of the 23 csproj `` values is +determined) and any change to the published version numbers. Doing that now, before choosing between +consolidating to a single line (option A) and keeping per-.NET `PackageId`s (option B), would risk rework. + +## Implementation status (Phase 2 — done) + +Extracted the shared relational implementation into a **single package**, +`eQuantic.Core.Data.EntityFramework.Relational`, referenced by the 3 SQL providers. + +| Finding | Fix | +|--------|----------| +| **Structural duplication** (§4) | `RelationalSqlExecutor`, `RelationalUnitOfWork`/`RelationalUnitOfWork`, `RelationalSet` and the internal `ExpressionConverter`/`SqlConfigurationExtensions` now live once in the shared package. Each provider keeps thin `Set` and `UnitOfWork` subclasses plus `DefaultUnitOfWork`, so the consumer-facing types stay in their namespaces. **~2,200 fewer lines of duplicated source.** | +| **P3 root cause** | The only genuine dialect difference — stored procedures use `EXEC` on SQL Server and the ANSI `CALL` elsewhere — is a single `BuildProcedureSql` virtual, overridden only by SQL Server. The copy-paste that let `EXEC`/`CALL` diverge is gone. | +| **PK3 (partial)** | MySql's per-framework variants are realigned to reference the multi-target base project (matching SqlServer/PostgreSql) so the shared multi-target project does not pull a second copy of the base assembly. | + +**Why a new package rather than the base package or shared source** (the plan originally proposed "base +package with dialect hooks", which the deeper analysis showed to be wrong): +- The base package must **not** depend on `Microsoft.EntityFrameworkCore.Relational` — the MongoDb provider + references the base and is not relational (it depends only on `Microsoft.EntityFrameworkCore` core + + `MongoDB.Driver`). Putting relational code in the base would add `Relational` to every MongoDb consumer. +- **Linked source** (``) does not work for public types shared across providers: the type + would be compiled into every provider assembly and collide if a consumer references two providers at once + (a scenario the library supports). +- Changing the types' **namespace** would break the public API — that belongs to the v5 work. + +A separate shared assembly is therefore the only clean option. It is multi-target only (net6–net10, one +version line) — no per-framework variants needed, because SqlServer/PostgreSql already reference the base +multi-target project everywhere, so the shared multi-target project composes with them with a single base +assembly (no duplicate). + +Note: the implementation-only public types `SqlExecutor`, the non-generic `UnitOfWork` and +`SqlConfigurationExtensions` move to the `Relational` namespace, and `GetEntityByIdSpecification` now takes +`RelationalUnitOfWork`. These are implementation types (not the consumer-facing `DefaultUnitOfWork` / +`UnitOfWork` / `Set`), but referencing them by name is a minor source break. + +**What a maintainer still owns:** the new `eQuantic.Core.Data.EntityFramework.Relational` package is a new +published package the SQL providers now depend on. Its version (`1.0.0`) and how it fits the versioning +scheme is part of the Part IV decision. + +**Next steps** (larger, architectural/breaking phases — awaiting a chosen approach): +- **Phase 3/4** — v5.0.0 of the contracts (`eQuantic.Core.Data`) and consolidation of the version lines on + nuget.org (see Parts III and IV) — this includes the versioning decision that also blocks MinVer. + +--- + +## Part I — Diagnosis + +### 1. Security + +| # | Sev. | Problem | Location | +|---|------|----------|-------| +| S1 | 🔴 Critical | **SQL injection**: `GetQueryParameters` interpolates values with `string.Format(" '{0}'", value)` without escaping single quotes; `string`/`Guid`/`DateTime` go raw into the SQL text, which reaches `FromSqlRaw`/`ExecuteSqlRaw` **with no `DbParameter`**. The function/procedure `name` is interpolated too. Identical file across the 3 SQL providers. | `SqlServer/Repository/SqlExecutor.cs:367,375-407` (same in PostgreSql and MySql) | +| S2 | 🟡 Medium | NuGet key exposed to any push: the workflow publishes with `secrets.nuget_key` on a push to **any branch**, with no protected environment and no tag/release gate. | `.github/workflows/dotnetcore.yml:3,66-67` | + +**S1 fix:** emit placeholders (`@p0`/`$1`/`?`) and pass real `DbParameter`s — the infrastructure already +exists in the same file (`SetCommand`, `SqlExecutor.cs:419-445`) and is simply ignored by those methods. + +### 2. Correctness bugs — Core (`eQuantic.Core.Data.EntityFramework`) + +| # | Sev. | Problem | Location | +|---|------|----------|-------| +| C1 | 🔴 | **UnitOfWork double-dispose**: `AsyncQueryableRepository.Dispose(bool)` calls `base.Dispose()` (which already disposes the UoW) and disposes the UoW again — caused by a shadowed `_disposed` field on the derived class. | `Repository/AsyncQueryableRepository.cs:756-773` + `Repository/QueryableRepository.cs:363-378` | +| C2 | 🔴 | **Inverted UoW ownership**: repositories dispose the **injected** UnitOfWork; with `AddGenericRepositories` the container disposes it too → dead DbContext for the other repositories in the scope. | `Repository/QueryableRepository.cs:374`; `Read/QueryableReadRepository.cs:22`; `Write/WriteRepository.cs:15` | +| C3 | 🔴 | **DI registration throws at runtime**: `ISqlUnitOfWork` is registered unconditionally even when the implementation does not implement it (MongoDb) → `InvalidCastException` on resolve. Line 75 also duplicates the `IQueryableUnitOfWork` registration (dead code). | `Repository/Extensions/ServiceCollectionExtensions.cs:74-75` | +| A1 | 🟠 | **Sync `All`/`Any` discard `configuration`**: `return this.All(specification.SatisfiedBy());` ignores includes/no-tracking/sorting. The async variants do it right — proof it is a bug, not design. | `Read/QueryableReadRepository.cs:208,233` | +| A2 | 🟠 | **`Get(id)` rejects valid default keys with the wrong exception**: `if (Equals(id, default(TKey))) throw new ArgumentNullException` — `Get(0)`/`Guid.Empty` throw on a non-null argument. | `Read/QueryableReadRepository.cs:254-257`; `Read/AsyncQueryableReadRepository.cs:357-360` | +| A3 | 🟠 | **Sync deferred vs async materialized**: sync `GetAll`/`GetFiltered`/`GetPaged` return a live `IQueryable` disguised as `IEnumerable` (double enumeration = 2 queries; late `ObjectDisposedException`), while the async ones do `ToListAsync`. Same method, divergent semantics. | `Read/QueryableReadRepository.cs:47,271,299,396` vs `Read/AsyncQueryableReadRepository.cs:44,336,732` | +| A4 | 🟠 | **Key as `Expression.Constant`**: `GetFindByKeyExpression` embeds the key value in the tree → EF does not parameterize; each id creates a new query-cache entry and SQL with a literal (plan-cache pollution). | `Repository/Extensions/DbContextExtensions.cs:25,43` | +| A5 | 🟠 | **Pagination without `OrderBy`**: `Skip/Take` with no guaranteed ordering → non-deterministic pages + EF's `RowLimitingOperationWithoutOrderBy` warning. | `Read/QueryableReadRepository.cs:395`; `Read/AsyncQueryableReadRepository.cs:729` | +| M-core | 🟡 | Several: NRT disabled across the package; **zero `ConfigureAwait(false)`** in the whole library; `SumAsync` (~24 overloads) with no `CancellationToken` and no null validation; `AddRepository` ignores the configured lifetime; uncached reflection on the hot `Get(id, config)` path; `GetAllAsync` injects a redundant `Where(_ => true)`; inconsistent tracking between `Get(id)` (uses `Find`) and `Get(id, config)` (uses a query). | see the per-file detailed report | + +### 3. Correctness bugs — Providers + +| # | Sev. | Problem | Location | +|---|------|----------|-------| +| P1 | 🔴 | **MongoDb operates against the wrong database**: `GetDatabase(_collectionName)` uses the **collection** name as the **database** name → `DeleteMany`/`UpdateMany` run against a non-existent database and return `0` **silently**. | `MongoDb/Repository/Set.cs:129-131` | +| P2 | 🔴 | **MongoDb `UpdateDefinitionBuilder` writes a constant**: `x => new E { Count = x.Count + 1 }` is compiled and invoked against `Activator.CreateInstance(...)` (a default instance) → writes `0+1=1` on every document instead of incrementing. Silent corruption. | `MongoDb/UpdateDefinitionBuilder.cs:38-43` | +| P3 | 🟠 | **`EXEC` (T-SQL) copied to PG/MySQL**: `GetQueryProcedure` returns `$"EXEC {name}..."`; PostgreSQL/MySQL require `CALL` → `ExecuteProcedure` fails at runtime. Direct evidence of the copy-paste. | `PostgreSql/Repository/SqlExecutor.cs:367`; `MySql/…:367` | +| P4 | 🟠 | **`FromSqlRaw` receives an `IEnumerable` as 1 parameter**: `FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value))` — the `Select` becomes a single element of the `params object[]`. Missing `.ToArray()`. | `SqlExecutor.cs:219` (3 SQL providers) | +| P5 | 🟠 | **Fragile `ExpressionConverter`**: `RewriteBinding` does not rebind the parameter → updates referencing the entity throw at runtime; `GetMethods().…Single(...)` breaks if EF adds a `SetProperty` overload; no cache. | `SqlServer/ExpressionConverter.cs:50-68,137-155` (3 SQL providers) | +| P6 | 🟡 | **MongoDb: silent returns**: without an `IMongoClient` in DI, `GetCollection()` returns null and the bulk ops return `0` without throwing. `IsSimpleType` does not cover `Guid`/`DateTimeOffset`/collections → discarded updates or `TargetParameterCountException`. | `MongoDb/Repository/Set.cs:29-65`; `UpdateDefinitionBuilder.cs:45-73` | +| P7 | 🟡 | **`SqlExecutor.Dispose` destroys the injected `DbContext`** (double-dispose in the DI scope). `IsMigrating` is a `static` shared across every context in the process. Unbounded `do/while` retry loop in `CommitAndRefreshChanges`. | `SqlExecutor.cs:483-497`; `UnitOfWork.cs:17,43-91` | +| P8 | 🟡 | **Orphaned `GetEntityByIdSpecification`**: exists only in SqlServer, nothing in it is SQL Server-specific (it delegates to the base `DbContextExtensions`), zero uses in the repo. Should be in the base or deprecated. | `SqlServer/Specifications/GetEntityByIdSpecification.cs` | + +### 4. Structural duplication + +| File | SqlServer | PostgreSql | MySql | MongoDb | +|---|---|---|---|---| +| `SqlExecutor.cs` | 498 lines | **identical** | **identical** | — | +| `UnitOfWork.cs` | 266 | **identical** | **identical** | ~180 same | +| `ExpressionConverter.cs` | 175 | **identical** | **identical** | — | +| `Set.cs` | 287 | 268 (= SqlServer minus the legacy blocks) | **identical to PG** | ~50 same | +| `SqlConfigurationExtensions.cs` | 14 | **identical** | **identical** | — | + +**~2,400–2,500 redundant lines.** The only genuine dialect differences in `SqlExecutor` are 2 lines +(`GetQueryFunction`/`GetQueryProcedure`). It is not merely cosmetic debt: the P3 bug (`EXEC`→`CALL`) exists +precisely because the file was copied without adapting the dialect. + +### 5. Packaging and versioning + +| # | Sev. | Problem | Location | +|---|------|----------|-------| +| PK1 | 🔴 | **Parallel version lines on the same `PackageId`**: 4.x multi-target + 6.x/7.x/8.x/9.x/10.x single-target. NuGet sees a single timeline → `dotnet add package` pulls 10.0.2 (net10-only) and breaks restore on net6–net9; Dependabot suggests an impossible upgrade; the major stops meaning "breaking" (it means TFM). | 21 csproj in `src/`; confirmed on nuget.org | +| PK2 | 🔴 | **Dependency-graph bug**: `MySql.Net10.csproj` references the **Net9** core → the MySql 10.0.2 package declares a dependency on core `>= 9.1.2` (net9-only). SqlServer/PG/MongoDb NetX reference the multi-target core 4.4.2 — the families are already crossed. | `MySql/…MySql.Net10.csproj:66-67` | +| PK3 | 🔴 | **Pomelo 9 over EF Core 10**: MySql net10.0 uses `Pomelo.EntityFrameworkCore.MySql 9.0.0` (built for EF 9) with `Microsoft.EntityFrameworkCore 10.0.3`. There is no stable Pomelo 10 — risk of binary incompatibility. | `MySql/…MySql.csproj` (net10 block) | +| PK4 | 🟠 | **Shared `obj/`/`bin/` directories**: several csproj files in the same folder without `BaseIntermediateOutputPath` → `project.assets.json` overwritten on each restore; parallel builds (`dotnet build -m` of the solution) are a declared race. | `src/*/` with multiple csproj | +| PK5 | 🟠 | **MongoDb main package misaligned**: `Version 8.1.2.0`, `net8.0` only — there is no MongoDb in the 4.x family or a multi-target one; a net9/net10 consumer gets the net8 build. | `MongoDb/…MongoDb.csproj:7,9` | +| PK6 | 🟠 | **Rolling `AssemblyVersion`** (changes on each patch) → in a diamond between providers compiled against different core lines, risk of `MissingMethodException`/`FileLoadException`. | all csproj `:27` | +| PK7 | 🟡 | **Dead and broken MSBump**: `build/MSBump.props` imports itself (circular), `MSBump.targets` calls `BumpVersion` with no `UsingTask`, `Directory.Build.targets` is in `build/` (not an ancestor of `src/`, never applied), and its own comment says it is obsolete since NuGet 4.6. None of it is imported today. When it worked, it produced non-deterministic per-build versions. | `build/*` | + +### 6. Contracts (`eQuantic.Core.Data`) + +| # | Sev. | Problem | Location | +|---|------|----------|-------| +| K1 | 🟠 | **Combinatorial explosion**: `IAsyncReadRepository` = **100 members**; `IReadRepository` = 56; `ISqlUnitOfWork` ≈ 47 (impossible to implement by hand — defeats the purpose of a contract). `GetPagedAsync` = 18 overloads; `SumAsync` = 30. The most recent commit *added* 60 Sum overloads — the trend is worsening. | `Read/IAsyncReadRepository.cs`; `Sql/ISqlUnitOfWork.cs` | +| K2 | 🟠 | **`TUnitOfWork` as a type parameter** enables a single member (`UnitOfWork { get; }`) but contaminates the whole hierarchy (~24 interfaces for 1 concept) and creates a circular UoW↔repository coupling. Inconsistent variance sync vs async. | `Repository/IRepository.cs:26,55`; `IAsyncRepository.cs:39` | +| K3 | 🟠 | **Pagination without metadata**: `GetPaged*` returns raw `IEnumerable`, with no total/page → the consumer does a separate `Count()` (2 non-atomic round-trips). A `PagedResult` is missing. | `Read/IReadRepository.cs:267-314` | +| K4 | 🟡 | **EF leaking into the contract**: `ISqlUnitOfWork` declares `GetPendingMigrations`/`UpdateDatabase`/`Attach` — contradicting the "persistence ignorance" its own XML docs claim. `IdentityGenerator` and `MigrationAttribute` are implementation in a contracts package. | `Sql/ISqlUnitOfWork.cs:23-127`; `IdentityGenerator.cs`; `Migration/MigrationAttribute.cs` | +| K5 | 🟡 | **`CancellationToken` missing** on 30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`. NRT disabled (`Get*` returns `Task` with no null annotation). No `IAsyncEnumerable`/streaming. | `Read/IAsyncReadRepository.cs`; `Write/IAsyncWriteRepository.cs` | +| K6 | 🟡 | **`new()` constraint everywhere** (hostile to DDD) and `IEntity`/`IEntity` do not tie the repository's `TKey` to the entity's real key (`IRepository` compiles even if the key is `int`). Should be `where TEntity : IEntity`. | `Repository/IRepository.cs:40` | +| K7 | 🟢 | **Latent bug**: `IdentityGenerator.GuidRegex` contains 2 invisible zero-width characters (U+200C and U+200B) inside the `[0-9…a-fA-F]{12}` class — a copy-paste artifact (confirmed by a byte dump). Dead `eQuantic.Core` dependency (no file imports it). Typo `mintute` in `MigrationAttribute`. | `IdentityGenerator.cs:7`; `core-data.csproj` | + +### 7. Process, CI and tests + +| # | Sev. | Problem | Location | +|---|------|----------|-------| +| Q1 | 🔴 | **CI publishes on any push, without tests**: `on: [push]` → `dotnet nuget push` on every push to any branch, with the NuGet key. **No `dotnet test`** runs before publishing. | `.github/workflows/dotnetcore.yml:3,66-67` | +| Q2 | 🔴 | **Placebo tests**: `UnitTest1.cs` is an `Assert.Pass()`. There is no coverage of any bug above. | `tests/…Tests/UnitTest1.cs` | +| Q3 | 🟡 | 21 sequential `dotnet build` steps instead of the solution; outdated actions (`checkout@v3`, `setup-dotnet@v3`); unnecessary `windows-latest`; no `dotnet test`, cache, `global.json`, deterministic pack (`ContinuousIntegrationBuild`), symbols (snupkg) or provenance. | `dotnetcore.yml` | +| Q4 | 🟡 | No central `Directory.Build.props`/`Directory.Packages.props`: metadata and package versions repeated across 21 csproj (the real source of PK2/PK3). No consistent NRT/`GenerateDocumentationFile` (packages ship to NuGet with no IntelliSense). The README says "Version 4.4.0" and does not explain the version matrix; `Repository.md` uses `IContainer`/pre-DI service-locator. | repo root | + +--- + +## Part II — Phased execution plan + +### Phase 0 — Harden the pipeline (days, no production code) — **done** + +Prerequisite for everything: stop publishing by accident and have a safety net. + +1. **Split CI from Release.** `ci.yml` on `push`/`pull_request`: `restore` → `build` → **`dotnet test`** → + `dotnet pack` as an artifact (no push). `release.yml` only on `push: tags: ['v*']`, with the + `nuget_key` in a **protected GitHub Environment**. +2. Update the actions to v4; move to `ubuntu-latest`; add a NuGet cache and `global.json`. +3. **Delete `build/`** (dead MSBump). MinVer adoption is deferred until the versioning decision (Part IV). + +### Phase 1 — Non contract-breaking fixes (current line, patch/minor) — **done** + +Ship now, without touching `eQuantic.Core.Data`. Each covered by a test. See the status table above. + +### Phase 2 — Structural de-duplication (minor, internal refactor) — **done** + +Extracted the shared relational implementation into a new +`eQuantic.Core.Data.EntityFramework.Relational` package (see the status section above for why a new package +rather than the base package). Each provider becomes thin subclasses + a dialect override. Removes ~2,200 +lines and kills the P3 bug class at the root. **Consumer-facing types keep their namespaces**; only a few +implementation-only types move. + +### Phase 3 — Contracts redesign v5.0.0 (deliberately breaking — see Part III) + +Consolidate the surface via *options objects*, introduce `PagedResult`, a uniform `CancellationToken`, +annotated NRT, `where TEntity : IEntity`, remove `TUnitOfWork` from the hierarchy and the EF-specific +content from the contracts. Reimplement on the EF package (which becomes ~10× smaller). + +### Phase 4 — Migration on nuget.org (see Part IV) + +Consolidate the version lines, deprecate the old ones without breaking anyone who already depends on them, +and publish the compatibility matrix. + +--- + +## Part III — Changes requiring a contract break in `eQuantic.Core.Data` + +General rule for the contracts package: **adding** a member to an interface already breaks every external +implementer (mocks, fakes, decorators, plus the EF impl itself), and **changing a signature/removing** +breaks callers too. Almost every fundamental improvement is therefore a v5.0.0. What requires breaking: + +1. **`CancellationToken` on the members that lack it** (30 `SumAsync`, `AddAsync`/`MergeAsync`/`ModifyAsync`/`RemoveAsync`, `LoadCollectionAsync`). Partial non-breaking route: *default interface methods* (DIM) delegating to the existing overload — viable because all TFMs are ≥ net6.0, but it crystallizes the overload explosion. +2. **Consolidate overloads into options objects** (`QueryOptions` absorbing filter/specification/config; `PageRequest`): remove the 18 `GetPagedAsync`, the 60 `Sum*` etc. Reduces `IAsyncReadRepository` from 100 to ~12 members. This is the heart of v5. +3. **`PagedResult` instead of `IEnumerable`** for pagination: a return-type change — hard breaking (not even DIM saves it; would require a new method with a different name, e.g. `QueryPagedAsync`). +4. **Remove `TUnitOfWork` from the hierarchy** (collapse `IRepository` into `IRepository` + `IUnitOfWork UnitOfWork { get; }`): removes ~10 public interfaces; the EF package references those arities in `GetRepository`. +5. **`where TEntity : IEntity` constraint** and/or removing `new()`: changes generic constraints — source+binary breaking. +6. **Split sync/async in `IUnitOfWork`** and **remove `ExecuteTransactionAsync` from `ISqlExecutor`** (an async method on the "sync" interface). +7. **Move `GetPendingMigrations`/`UpdateDatabase`/`MigrationAttribute`/`IdentityGenerator` to the EF package**: removes public types/members from the contract — a real break (the contract→EF direction rules out `[TypeForwardedTo]`). +8. **Annotated NRT** (`TEntity?` on `Get`/`GetFirst`/`GetSingle`): technically only produces new warnings — the cheapest break; requires both packages to be annotated together to stay consistent. + +**Recommendation:** treat the next contract version as a **deliberately breaking v5.0.0** and reimplement +the EF package on top of it, rather than stacking DIMs over a 100-member surface. The EF package's +maintenance cost — today implementing ~156 members per provider × 4 providers — drops by an order of +magnitude. + +--- + +## Part IV — Versioning strategy on nuget.org + +The PK1 problem has two coherent exits. The recommended one is (A). + +**(A) Consolidate into a single multi-target line per `PackageId` (recommended).** +One multi-target `.csproj` per package (`net8.0;net9.0;net10.0` — net6/net7 are EOL), a single version line, +resumed **above** the highest already published so the timeline becomes increasing and monotonic again +(e.g. **11.0.0**, or 5.0.0 if you accept the numeric "latest" dropping — which would confuse anyone already +on 10.x). Multi-target already delivers the right binary per TFM inside a single `.nupkg` — exactly what the +parallel-lines scheme tries to emulate by hand. This fixes PK1, PK2, PK4, PK5 and Q4 at once. + +**(B) Keep per-.NET families, but with distinct `PackageId`s.** +E.g. `eQuantic.Core.Data.EntityFramework.Net8`. It is the only way NuGet treats the families as independent +lines (each id's "latest" is correct for its TFM). Cost: it fragments the consumer ecosystem and discovery +on nuget.org, and multiplies the packages to maintain. Only worth it if there is a strong reason to freeze +each TFM to its own API. + +**Migration without breaking anyone already on the old versions** (applies to A and B): +- **Never** unpublish (`unlist` keeps restore working for pinned versions; `delete` breaks it). Use + **deprecation** on nuget.org (`Legacy`/`Other`) on the old versions, pointing to the new one. +- Publish the **compatibility matrix** (TFM × package × version) in the README — today the README says + "Version 4.4.0" and explains none of it. +- Align the core + 4 providers to release **always together, at the same version** (resolves the + `AssemblyVersion` diamonds). +- Only then resume the consolidated numbering and point the release CI at tags. + +Note: the new `eQuantic.Core.Data.EntityFramework.Relational` package introduced in Phase 2 must join +whichever scheme is chosen here. + +--- + +## Appendix — Suggested order (what to do first) + +1. **Phase 0** (pipeline) — unblocks everything safely. ✅ done +2. **S1, C1, C2, C3, P1, P2** — the 6 🔴 security/runtime findings, with tests. ✅ done +3. **Rest of Phase 1** (🟠 findings) on the current line. ✅ done +4. **Phase 2** (de-dup) — cheap, high return, no break. ✅ done +5. **Phases 3–4** — plan the contract v5.0.0 and the version consolidation as a separate milestone, + communicated to consumers in advance. diff --git a/eQuantic.Core.Data.EntityFramework.sln b/eQuantic.Core.Data.EntityFramework.sln index 7630e09..0f2964d 100644 --- a/eQuantic.Core.Data.EntityFramework.sln +++ b/eQuantic.Core.Data.EntityFramework.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26430.6 @@ -14,52 +14,20 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "eQuantic.Core.Data.EntityFramework", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj", "{CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net6", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net6.csproj", "{20175F0B-5566-4213-A60E-435A9458B018}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net7", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net7.csproj", "{BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net8", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net8.csproj", "{AB04C373-0DBD-4B6F-811D-D668851BBAD5}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.csproj", "{91233051-EE7E-4CBB-8FFD-B900ABBACBBE}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SqlServer", "SqlServer", "{5B80C186-5E75-413B-B825-0B5B2FE9A3E4}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MongoDb", "MongoDb", "{F613B6F2-C420-4E72-981A-47A20A538BC6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb", "src\eQuantic.Core.Data.EntityFramework.MongoDb\eQuantic.Core.Data.EntityFramework.MongoDb.csproj", "{B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net6", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj", "{CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net7", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj", "{068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net8", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj", "{A5610597-89E9-4BE1-8B87-31B7B7E11473}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Net9", "src\eQuantic.Core.Data.EntityFramework.MongoDb\eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj", "{730DEB2B-3B8C-49A4-BD2D-12A274C8790A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net9", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj", "{8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net9", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net9.csproj", "{99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MySql", "MySql", "{9D499CE2-6681-4F5D-8F47-F634EFF79132}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MySql", "src\eQuantic.Core.Data.EntityFramework.MySql\eQuantic.Core.Data.EntityFramework.MySql.csproj", "{7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MySql.Net8", "src\eQuantic.Core.Data.EntityFramework.MySql\eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj", "{C7367715-9273-460F-BE2C-1BB06095E45C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MySql.Net9", "src\eQuantic.Core.Data.EntityFramework.MySql\eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj", "{CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PostgreSql", "PostgreSql", "{A6FAA830-E2CB-48CD-91C6-E0D75BFD8291}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.csproj", "{B67128D8-373F-4215-9DCE-85594BEAC0E2}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql.Net8", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj", "{46781B67-3B50-4DC9-BA64-6713528F605A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql.Net9", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj", "{715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net10", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net10.csproj", "{7D85EC36-84C6-4429-BE02-291F046BACA4}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Net10", "src\eQuantic.Core.Data.EntityFramework.SqlServer\eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj", "{D40D237B-5C44-4271-9DCC-53E07DE4342F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.PostgreSql.Net10", "src\eQuantic.Core.Data.EntityFramework.PostgreSql\eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj", "{9BB6467D-1212-4762-96C8-0C1B6E10109F}" @@ -74,146 +42,223 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.SqlServer.Tests", "tests\eQuantic.Core.Data.EntityFramework.SqlServer.Tests\eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj", "{A730FE2E-2972-41F6-AAF7-3AEE017A7418}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Tests", "tests\eQuantic.Core.Data.EntityFramework.MongoDb.Tests\eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj", "{8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Relational", "src\eQuantic.Core.Data.EntityFramework.Relational\eQuantic.Core.Data.EntityFramework.Relational.csproj", "{A6953967-90B5-485D-956E-A62EFE67423F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eQuantic.Core.Data.EntityFramework.MongoDb", "eQuantic.Core.Data.EntityFramework.MongoDb", "{6A49CAD3-8848-795E-F57A-8F1C385ECFED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.MongoDb.Net8", "src\eQuantic.Core.Data.EntityFramework.MongoDb\eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj", "{2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "eQuantic.Core.Data.EntityFramework", "eQuantic.Core.Data.EntityFramework", "{BEB4D7A8-0559-D436-17A0-F8FA25A80731}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net8", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net8.csproj", "{0CAE00C1-4547-4DDA-A526-A4BBB78898A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eQuantic.Core.Data.EntityFramework.Net10", "src\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.Net10.csproj", "{B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2}.Release|Any CPU.Build.0 = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Debug|Any CPU.Build.0 = Debug|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|Any CPU.ActiveCfg = Release|Any CPU - {20175F0B-5566-4213-A60E-435A9458B018}.Release|Any CPU.Build.0 = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F}.Release|Any CPU.Build.0 = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB04C373-0DBD-4B6F-811D-D668851BBAD5}.Release|Any CPU.Build.0 = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE}.Release|Any CPU.Build.0 = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D}.Release|Any CPU.Build.0 = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B}.Release|Any CPU.Build.0 = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F}.Release|Any CPU.Build.0 = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x64.Build.0 = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Debug|x86.Build.0 = Debug|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|Any CPU.Build.0 = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A}.Release|Any CPU.Build.0 = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344}.Release|Any CPU.Build.0 = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5}.Release|Any CPU.Build.0 = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8}.Release|Any CPU.Build.0 = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x64.ActiveCfg = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x64.Build.0 = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x86.ActiveCfg = Release|Any CPU + {A5610597-89E9-4BE1-8B87-31B7B7E11473}.Release|x86.Build.0 = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x64.Build.0 = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Debug|x86.Build.0 = Debug|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|Any CPU.ActiveCfg = Release|Any CPU {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|Any CPU.Build.0 = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD}.Release|Any CPU.Build.0 = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B67128D8-373F-4215-9DCE-85594BEAC0E2}.Release|Any CPU.Build.0 = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x64.ActiveCfg = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x64.Build.0 = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x86.ActiveCfg = Release|Any CPU + {C7367715-9273-460F-BE2C-1BB06095E45C}.Release|x86.Build.0 = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x64.ActiveCfg = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x64.Build.0 = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x86.ActiveCfg = Debug|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Debug|x86.Build.0 = Debug|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|Any CPU.ActiveCfg = Release|Any CPU {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|Any CPU.Build.0 = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6}.Release|Any CPU.Build.0 = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7D85EC36-84C6-4429-BE02-291F046BACA4}.Release|Any CPU.Build.0 = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x64.ActiveCfg = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x64.Build.0 = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x86.ActiveCfg = Release|Any CPU + {46781B67-3B50-4DC9-BA64-6713528F605A}.Release|x86.Build.0 = Release|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x64.ActiveCfg = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x64.Build.0 = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x86.ActiveCfg = Debug|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Debug|x86.Build.0 = Debug|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|Any CPU.ActiveCfg = Release|Any CPU {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|Any CPU.Build.0 = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x64.ActiveCfg = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x64.Build.0 = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x86.ActiveCfg = Release|Any CPU + {D40D237B-5C44-4271-9DCC-53E07DE4342F}.Release|x86.Build.0 = Release|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x64.ActiveCfg = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x64.Build.0 = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x86.ActiveCfg = Debug|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Debug|x86.Build.0 = Debug|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|Any CPU.ActiveCfg = Release|Any CPU {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|Any CPU.Build.0 = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x64.ActiveCfg = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x64.Build.0 = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x86.ActiveCfg = Release|Any CPU + {9BB6467D-1212-4762-96C8-0C1B6E10109F}.Release|x86.Build.0 = Release|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x64.ActiveCfg = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x64.Build.0 = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Debug|x86.Build.0 = Debug|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|Any CPU.Build.0 = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x64.ActiveCfg = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x64.Build.0 = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x86.ActiveCfg = Release|Any CPU + {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748}.Release|x86.Build.0 = Release|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x64.ActiveCfg = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x64.Build.0 = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x86.ActiveCfg = Debug|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Debug|x86.Build.0 = Debug|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|Any CPU.ActiveCfg = Release|Any CPU {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|Any CPU.Build.0 = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x64.ActiveCfg = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x64.Build.0 = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x86.ActiveCfg = Release|Any CPU + {B5E862DD-A02A-4E26-AA83-687C608C4203}.Release|x86.Build.0 = Release|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x64.Build.0 = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Debug|x86.Build.0 = Debug|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|Any CPU.Build.0 = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x64.ActiveCfg = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x64.Build.0 = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x86.ActiveCfg = Release|Any CPU + {D0A7C1F3-94B3-4353-9C14-827ABBAD649D}.Release|x86.Build.0 = Release|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x64.ActiveCfg = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x64.Build.0 = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x86.ActiveCfg = Debug|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Debug|x86.Build.0 = Debug|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|Any CPU.ActiveCfg = Release|Any CPU {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|Any CPU.Build.0 = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x64.ActiveCfg = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x64.Build.0 = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x86.ActiveCfg = Release|Any CPU + {A730FE2E-2972-41F6-AAF7-3AEE017A7418}.Release|x86.Build.0 = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x64.Build.0 = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Debug|x86.Build.0 = Debug|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|Any CPU.Build.0 = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x64.ActiveCfg = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x64.Build.0 = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x86.ActiveCfg = Release|Any CPU + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69}.Release|x86.Build.0 = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x64.ActiveCfg = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x64.Build.0 = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Debug|x86.Build.0 = Debug|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|Any CPU.Build.0 = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x64.ActiveCfg = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x64.Build.0 = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x86.ActiveCfg = Release|Any CPU + {A6953967-90B5-485D-956E-A62EFE67423F}.Release|x86.Build.0 = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x64.ActiveCfg = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x64.Build.0 = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x86.ActiveCfg = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Debug|x86.Build.0 = Debug|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|Any CPU.Build.0 = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x64.ActiveCfg = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x64.Build.0 = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x86.ActiveCfg = Release|Any CPU + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94}.Release|x86.Build.0 = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x64.Build.0 = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Debug|x86.Build.0 = Debug|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|Any CPU.Build.0 = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x64.ActiveCfg = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x64.Build.0 = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x86.ActiveCfg = Release|Any CPU + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2}.Release|x86.Build.0 = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x64.ActiveCfg = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x64.Build.0 = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x86.ActiveCfg = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Debug|x86.Build.0 = Debug|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|Any CPU.Build.0 = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x64.ActiveCfg = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x64.Build.0 = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x86.ActiveCfg = Release|Any CPU + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {CB4BD082-A5A5-4E9D-A226-1C7B8C505CC2} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {20175F0B-5566-4213-A60E-435A9458B018} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {BFDD8BE6-912B-4593-8F53-C6B6B3F8490F} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {AB04C373-0DBD-4B6F-811D-D668851BBAD5} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {91233051-EE7E-4CBB-8FFD-B900ABBACBBE} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} {F613B6F2-C420-4E72-981A-47A20A538BC6} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {B1BE22DD-76F6-45BE-BBD8-6502B8CCB47D} = {F613B6F2-C420-4E72-981A-47A20A538BC6} - {CD7B2B69-FCC3-49FB-9EE9-0078EC30507B} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} - {068DEA0E-ED2C-43FE-9552-7D33F88B8C8F} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} {A5610597-89E9-4BE1-8B87-31B7B7E11473} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} - {730DEB2B-3B8C-49A4-BD2D-12A274C8790A} = {F613B6F2-C420-4E72-981A-47A20A538BC6} - {8FB6D5FF-A9A0-4A38-AA24-9746E3ADD344} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} - {99E041B1-FD74-4D9E-97E0-00CA10E2E8A5} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {9D499CE2-6681-4F5D-8F47-F634EFF79132} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {7921DCBF-FAFF-4080-8AA0-A00C0548FDD8} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} {C7367715-9273-460F-BE2C-1BB06095E45C} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} - {CD4AFD53-E76D-4181-85BA-B14E37DEFAFD} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} - {B67128D8-373F-4215-9DCE-85594BEAC0E2} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} {46781B67-3B50-4DC9-BA64-6713528F605A} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} - {715C2497-F3FF-468D-ACF6-A3BB5C18D2D6} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} - {7D85EC36-84C6-4429-BE02-291F046BACA4} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} {D40D237B-5C44-4271-9DCC-53E07DE4342F} = {5B80C186-5E75-413B-B825-0B5B2FE9A3E4} {9BB6467D-1212-4762-96C8-0C1B6E10109F} = {A6FAA830-E2CB-48CD-91C6-E0D75BFD8291} {A3EED73E-6A4B-4C7C-9705-BE6FDEA9B748} = {9D499CE2-6681-4F5D-8F47-F634EFF79132} {B5E862DD-A02A-4E26-AA83-687C608C4203} = {F613B6F2-C420-4E72-981A-47A20A538BC6} {D0A7C1F3-94B3-4353-9C14-827ABBAD649D} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} {A730FE2E-2972-41F6-AAF7-3AEE017A7418} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} + {8BFE9861-4A05-4D69-BC21-6FDB5C4B6A69} = {EE71F02F-413D-4BC0-832F-726B6D8C0AD7} + {A6953967-90B5-485D-956E-A62EFE67423F} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} + {6A49CAD3-8848-795E-F57A-8F1C385ECFED} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} + {2AD27AB3-FA4A-45B8-83AC-A83156FC4D94} = {6A49CAD3-8848-795E-F57A-8F1C385ECFED} + {BEB4D7A8-0559-D436-17A0-F8FA25A80731} = {0CA2F610-6D57-4D1F-92E3-EDCCEDAD8297} + {0CAE00C1-4547-4DDA-A526-A4BBB78898A2} = {BEB4D7A8-0559-D436-17A0-F8FA25A80731} + {B8AA3B17-808A-4D8B-8C1F-EB42E9C5C354} = {BEB4D7A8-0559-D436-17A0-F8FA25A80731} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {83FECDD1-8A97-40B1-8529-3D5216E674C3} diff --git a/global.json b/global.json new file mode 100644 index 0000000..512142d --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs index d51ccc0..dbe5b02 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/Set.cs @@ -1,134 +1,100 @@ using System.Linq.Expressions; using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using MongoDB.EntityFrameworkCore.Extensions; +using MongoDB.EntityFrameworkCore.Infrastructure; namespace eQuantic.Core.Data.EntityFramework.MongoDb.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +public class Set : SetBase where TEntity : class, IEntity { private readonly IServiceProvider _serviceProvider; private readonly string? _collectionName; + private readonly string? _databaseName; private IMongoDatabase? _mongoDatabase; - + public Set(IServiceProvider serviceProvider, DbContext context) : base(context) { _serviceProvider = serviceProvider; var entityType = context.Model.FindEntityType(typeof(TEntity)); _collectionName = entityType?.GetCollectionName(); + + // Resolve the database name from the DbContext configuration up front (the context is only + // available here), NOT from the collection name. + _databaseName = context.GetService() + .FindExtension()?.DatabaseName; } - + public override long DeleteMany(Expression> filter) { - var collection = GetCollection(); - if (collection == null) return 0; - - var result = collection.DeleteMany(filter); + var result = GetCollection().DeleteMany(filter); return result.DeletedCount; } public override async Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { - var collection = GetCollection(); - if (collection == null) return 0; - var mongoFilter = Builders.Filter.Where(filter); - var result = await collection.DeleteManyAsync(mongoFilter, cancellationToken); - + var result = await GetCollection().DeleteManyAsync(mongoFilter, cancellationToken).ConfigureAwait(false); + return result.DeletedCount; } public override long UpdateMany(Expression> filter, Expression> updateExpression) { - var collection = GetCollection(); - if (collection == null) return 0; - var mongoFilter = Builders.Filter.Where(filter); var updateDefinition = UpdateDefinitionBuilder.BuildUpdateDefinition(updateExpression); if (updateDefinition == null) return 0; - - var result = collection.UpdateMany(mongoFilter, updateDefinition); + + var result = GetCollection().UpdateMany(mongoFilter, updateDefinition); return result.ModifiedCount; } public override async Task UpdateManyAsync(Expression> filter, Expression> updateExpression, CancellationToken cancellationToken = default) { - var collection = GetCollection(); - if (collection == null) return 0; - var mongoFilter = Builders.Filter.Where(filter); var updateDefinition = UpdateDefinitionBuilder.BuildUpdateDefinition(updateExpression); if (updateDefinition == null) return 0; - - var result = await collection.UpdateManyAsync(mongoFilter, updateDefinition, null, cancellationToken); + + var result = await GetCollection().UpdateManyAsync(mongoFilter, updateDefinition, null, cancellationToken).ConfigureAwait(false); return result.ModifiedCount; } - public override IQueryable GetQueryable(Action configuration, Func, IQueryable> internalQueryAction) + private IMongoCollection GetCollection() { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - IQueryable query = this; - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } + return GetDatabase().GetCollection(_collectionName); + } - if (queryableConfig != null) + private IMongoDatabase GetDatabase() + { + if (_mongoDatabase != null) { - query = queryableConfig.BeforeCustomization.Invoke(query); + return _mongoDatabase; } - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) + // The database name comes from the DbContext configuration (UseMongoDB(..., databaseName)), + // NOT from the collection name. Using the collection name here meant every bulk operation ran + // against the wrong database and silently affected zero documents. + if (string.IsNullOrEmpty(_databaseName)) { - query = query.OrderBy(config.SortingColumns.ToArray()); + throw new InvalidOperationException( + $"Could not resolve the MongoDB database name for '{typeof(TEntity).Name}'. " + + "Ensure the DbContext is configured with UseMongoDB(..., databaseName)."); } - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } + var client = _serviceProvider.GetService() + ?? throw new InvalidOperationException( + "No IMongoClient is registered in the service provider; bulk MongoDB operations " + + "(DeleteMany/UpdateMany) cannot be executed. Register an IMongoClient in DI."); - return query; - } - - private IMongoCollection? GetCollection() - { - _mongoDatabase ??= _serviceProvider.GetService()?.GetDatabase(_collectionName); - return _mongoDatabase?.GetCollection(_collectionName); + _mongoDatabase = client.GetDatabase(_databaseName); + return _mongoDatabase; } -} \ No newline at end of file +} diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs index 15f87d2..8e38967 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/Repository/UnitOfWork.cs @@ -18,7 +18,7 @@ public abstract class UnitOfWork : IQueryableUnitOfWork /// The context /// protected readonly DbContext Context; - + /// /// The disposed /// @@ -34,7 +34,7 @@ protected UnitOfWork(IServiceProvider serviceProvider, DbContext context) ServiceProvider = serviceProvider; Context = context; } - + public int Commit() { return Context.SaveChanges(); @@ -74,7 +74,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati { try { - changes = await Context.SaveChangesAsync(cancellationToken); + changes = await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); saveFailed = false; } @@ -92,7 +92,7 @@ public async Task CommitAndRefreshChangesAsync(CancellationToken cancellati public async Task CommitAsync(CancellationToken cancellationToken = default) { - return await Context.SaveChangesAsync(cancellationToken); + return await Context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } public int Commit(Action options) @@ -115,16 +115,16 @@ public Task CommitAsync(Action options, CancellationToken canc return CommitAsync(cancellationToken); } - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() + public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity { ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); } - public void Attach(TEntity item) where TEntity : class, IEntity, new() + public void Attach(TEntity item) where TEntity : class, IEntity { ((Set)InternalCreateSet()).Attach(item); } - + public void LoadCollection(TEntity item, Expression>> navigationProperty, Expression>? filter = null) where TEntity : class where TElement : class @@ -145,11 +145,11 @@ public async Task LoadCollectionAsync(TEntity item, { if (filter != null) { - await Context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); + await Context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); } else { - await Context.Entry(item).Collection(navigationProperty).LoadAsync(); + await Context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); } } @@ -179,42 +179,38 @@ public virtual SaveOptions GetSaveOptions() { return new SaveOptions(); } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork + + public virtual IRepository GetRepository() + where TEntity : class, IEntity { - var repo = ServiceProvider.GetRequiredService>(); - return repo; + return ServiceProvider.GetRequiredService>(); } - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); } - public Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => InternalCreateSet(); + public Data.Repository.ISet CreateSet() where TEntity : class, IEntity => InternalCreateSet(); - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); } - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); } - + internal DbContext GetDbContext() => Context; - - internal Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => + + internal Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity => new Set(ServiceProvider, Context); - + /// /// Disposes this instance /// @@ -223,7 +219,7 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } - + /// /// Disposes the disposing /// @@ -246,4 +242,4 @@ protected virtual void Dispose(bool disposing) public abstract class UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : UnitOfWork(serviceProvider, context) - where TDbContext : DbContext; \ No newline at end of file + where TDbContext : DbContext; diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs b/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs index 1a6a34e..364dc0f 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/UpdateDefinitionBuilder.cs @@ -37,11 +37,53 @@ internal static class UpdateDefinitionBuilder private static object? GetValueFromExpression(Expression expression, IReadOnlyList parameters) { + // The value is evaluated against a fresh default instance of the entity. Any assignment that + // reads the entity (e.g. x => x.Count + 1) would therefore be evaluated against Count == 0 and + // silently write the constant 1 to every matched document instead of incrementing it. Reject + // such expressions loudly rather than corrupting data; only constant/closure values are safe. + if (ReferencesParameter(expression, parameters)) + { + throw new NotSupportedException( + "The MongoDB update builder does not support update expressions that reference the " + + "entity being updated (e.g. x => x.Count + 1). Such an expression would be evaluated " + + "against a default instance and would overwrite documents with a constant value. Use a " + + "constant or captured value instead."); + } + var lambda = Expression.Lambda(expression, parameters); var compiledLambda = lambda.Compile(); return compiledLambda.DynamicInvoke(Activator.CreateInstance(parameters[0].Type)); } + private static bool ReferencesParameter(Expression expression, IReadOnlyList parameters) + { + var detector = new ParameterReferenceDetector(parameters); + detector.Visit(expression); + return detector.Found; + } + + private sealed class ParameterReferenceDetector : ExpressionVisitor + { + private readonly IReadOnlyList _parameters; + + public ParameterReferenceDetector(IReadOnlyList parameters) + { + _parameters = parameters; + } + + public bool Found { get; private set; } + + protected override Expression VisitParameter(ParameterExpression node) + { + if (_parameters.Contains(node)) + { + Found = true; + } + + return base.VisitParameter(node); + } + } + private static bool IsSimpleType(Type type) { return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime); diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj index 63a7812..2a64b88 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net10.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb - 10.0.2.0 - eQuantic Systems + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 10.0.2.0 - 10.0.2.0 - Icon.png - latest + 10.1.0.0 + 10.1.0.0 enable enable @@ -35,17 +21,17 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -53,8 +39,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj similarity index 64% rename from src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj rename to src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj index fe809e0..76c7c37 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net8.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and Mongo DB eQuantic.Core.Data.EntityFramework.MongoDb - 8.1.2.0 - eQuantic Systems + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.MongoDb eQuantic.Core.Data.EntityFramework.MongoDb @@ -13,21 +12,8 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 8.1.2.0 - 8.1.2.0 - Icon.png - latest + 8.2.0.0 + 8.2.0.0 enable enable @@ -35,17 +21,17 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -53,8 +39,6 @@ - - diff --git a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj deleted file mode 100644 index 3ca09eb..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MongoDb/eQuantic.Core.Data.EntityFramework.MongoDb.Net9.csproj +++ /dev/null @@ -1,70 +0,0 @@ - - - - - Core Data library for Entity Framework and Mongo DB - eQuantic.Core.Data.EntityFramework.MongoDb - 9.1.2.0 - eQuantic Systems - net9.0 - eQuantic.Core.Data.EntityFramework.MongoDb - eQuantic.Core.Data.EntityFramework.MongoDb - eQuantic;Core;Data;Library;Repository;Pattern;MongoDB - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 9.1.2.0 - 9.1.2.0 - Icon.png - latest - - enable - enable - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs deleted file mode 100644 index 08a7235..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Extensions/SqlConfigurationExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using eQuantic.Core.Data.Repository.Config; -using Microsoft.EntityFrameworkCore; - -namespace eQuantic.Core.Data.EntityFramework.MySql.Repository.Extensions; - -public static class SqlConfigurationExtensions -{ - private const int DefaultCommandTimeout = 60; - - public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) - { - return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs index 2e31064..957e526 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/Set.cs @@ -1,268 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +/// +/// MySQL entity set. The implementation lives in ; this type +/// is preserved for source compatibility. +/// +public class Set : RelationalSet where TEntity : class, IEntity { public Set(DbContext context) : base(context) { } - - public override long DeleteMany(Expression> filter) - { - return InternalDbSet.Where(filter).ExecuteDelete(); - } - - public override async Task DeleteManyAsync(Expression> filter, - CancellationToken cancellationToken = default) - { - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); - } - - public void LoadCollection(TChildEntity item, - Expression>> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Collection(selector).Load(); - } - - public void LoadCollection(TChildEntity item, string propertyName) - where TChildEntity : class - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - - public async Task LoadCollectionAsync(TChildEntity item, - Expression>> selector) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Collection(selector).LoadAsync(); - } - - public async Task LoadCollectionAsync(TChildEntity item, string propertyName) - where TChildEntity : class - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(); - } - - public void LoadProperties(TEntity entity, params string[] properties) - { - if (properties is not { Length: > 0 }) - { - return; - } - - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - LoadProperty(entity, property); - } - else - { - LoadCascade(props, entity); - } - } - } - - public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) - { - if (properties is { Length: > 0 }) - { - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - await LoadPropertyAsync(entity, property); - } - else - { - await LoadCascadeAsync(props, entity); - } - } - } - } - - public void LoadProperty(TChildEntity item, - Expression> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Reference(selector).Load(); - } - - public void LoadProperty(TChildEntity item, string propertyName) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - else - { - DbContext.Entry(item).Reference(propertyName).Load(); - } - } - - public async Task LoadPropertyAsync(TChildEntity item, - Expression> selector, CancellationToken cancellationToken = default) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken); - } - - public async Task LoadPropertyAsync(TChildEntity item, string propertyName, - CancellationToken cancellationToken = default) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken); - } - else - { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken); - } - } - - public override long UpdateMany(Expression> filter, - Expression> updateExpression) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); - } - - public override async Task UpdateManyAsync(Expression> filter, - Expression> updateExpression, CancellationToken cancellationToken = default) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken); - } - - private void LoadCascade(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - LoadProperty(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - LoadCascade(props, nextObj, index + 1); - } - } - - private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - await LoadPropertyAsync(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - await LoadCascadeAsync(props, nextObj, index + 1); - } - } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs deleted file mode 100644 index 5020a7b..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/SqlExecutor.cs +++ /dev/null @@ -1,498 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.MySql.Repository.Extensions; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; - -namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; - -/// -/// The sql executor class -/// -/// -/// -/// -[ExcludeFromCodeCoverage] -public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable -{ - /// - /// The context - /// - protected readonly DbContext Context; - - /// - /// The disposed - /// - protected bool Disposed; - - /// - /// The transaction - /// - protected IDbContextTransaction Transaction; - - /// - /// Initializes a new instance of the class - /// - /// The context - protected SqlExecutor(DbContext context) - { - Context = context; - } - - /// - /// Begins the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) - { - Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken); - } - - /// - /// Commits the transaction using the specified cancellation token - /// - /// The cancellation token - public virtual async Task CommitTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.CommitAsync(cancellationToken); - } - } - - /// - /// Rollbacks the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.RollbackAsync(cancellationToken); - } - } - - /// - public async Task> ExecuteRawSqlAsync(string sql, Func map, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken); - await using var result = await command.ExecuteReaderAsync(cancellationToken); - - var items = new List(); - while (await result.ReadAsync(cancellationToken)) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Executes the non query using the specified sql command - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteNonQuery(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - return command.ExecuteNonQuery(); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// The configuration. - /// The result - public TResult ExecuteFunction(string name, Action config = null) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set() - .FromSqlRaw(sql) - .FirstOrDefault(); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteCommand(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - var result = command.ExecuteScalar(); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// - /// The cancellation token - /// A task containing the int - public async Task ExecuteCommandAsync(string sqlCommand, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken); - var result = await command.ExecuteScalarAsync(cancellationToken); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// - /// The cancellation token - /// A task containing the result - public Task ExecuteFunctionAsync(string name, - Action config = null, - CancellationToken cancellationToken = default) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql) - .FirstOrDefaultAsync(cancellationToken); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// The configuration - /// The int - public int ExecuteProcedure(string name, Action config = null) - { - var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";"); - } - - /// - /// Executes the query using the specified sql query - /// - /// The entity - /// The sql query - /// The configuration. - /// An enumerable of t entity - public IEnumerable ExecuteQuery(string sqlQuery, Action config = null) where TEntity : class - { - var configuration = GetConfig(config); - var sql = ParseSql(sqlQuery, configuration); - return Context.Set().FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value)); - } - - /// - /// Executes the transaction using the specified operation - /// - /// The operation - /// - public void ExecuteTransaction(Action operation) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - strategy.Execute(() => { operation.Invoke((ISqlUnitOfWork)this); }); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// - /// The cancellation token - /// A task containing the int - public Task ExecuteProcedureAsync(string name, Action config = null, - CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - return ExecuteCommandAsync(GetQueryProcedure(name, configuration) + ";", config, cancellationToken); - } - - /// - public Task ExecuteTransactionAsync(Func operation, - CancellationToken cancellationToken = default) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - return strategy.ExecuteAsync(() => operation.Invoke((ISqlUnitOfWork)this)); - } - - /// - /// Begins the transaction - /// - public void BeginTransaction() - { - Transaction?.Dispose(); - Transaction = Context.Database.BeginTransaction(); - } - - /// - /// Commits the transaction - /// - public virtual void CommitTransaction() - { - Transaction?.Commit(); - } - - /// - /// Executes the raw sql using the specified sql - /// - /// The - /// The sql - /// The map - /// The configuration. - /// The items - public IEnumerable ExecuteRawSql(string sql, Func map, - Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - Context.Database.OpenConnection(); - using var result = command.ExecuteReader(); - - var items = new List(); - while (result.Read()) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Gets the transaction - /// - /// The db transaction - public DbTransaction GetTransaction() - { - return Transaction?.GetDbTransaction(); - } - - /// - /// Rollbacks the transaction - /// - public void RollbackTransaction() - { - Transaction?.Rollback(); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - public void UseTransaction(DbTransaction transaction) - { - Context.Database.UseTransaction(transaction); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - /// The cancellation token - public Task UseTransactionAsync(DbTransaction transaction, CancellationToken cancellationToken = default) - { - return Context.Database.UseTransactionAsync(transaction, cancellationToken); - } - - /// - /// Gets the query function using the specified name - /// - /// The name - /// The configuration. - /// The string - private static string GetQueryFunction(string name, SqlConfiguration config) - { - return $"SELECT {name}({GetQueryParameters(config.Parameters.ToArray())} )"; - } - - /// - /// Gets the query procedure using the specified name - /// - /// The name - /// The configuration. - /// The string - private static string GetQueryProcedure(string name, SqlConfiguration config) - { - return $"EXEC {name}{GetQueryParameters(config.Parameters.ToArray())}"; - } - - /// - /// Gets the query parameters using the specified parameters - /// - /// The parameters - /// The string - private static string GetQueryParameters(params ParamValue[] parameters) - { - var cmdBuilder = new StringBuilder(); - if (parameters is not { Length: > 0 }) - { - return cmdBuilder.ToString(); - } - - for (var i = 0; i < parameters.Length; i++) - { - if (i > 0) - { - cmdBuilder.Append(','); - } - - if (parameters[i].Value == null) - { - cmdBuilder.Append(" NULL"); - } - else - { - var fmt = " {0}"; - if (parameters[i].Value is Guid or string or DateTime) - { - fmt = " '{0}'"; - } - - cmdBuilder.Append(string.Format(fmt, parameters[i].Value)); - } - } - - return cmdBuilder.ToString(); - } - - private static string ParseSql(string sql, SqlConfiguration config) - { - return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; - } - /// - /// Sets the command using the specified command timeout - /// - /// The sql command - /// The command - /// The configuration. - private void SetCommand(string sqlCommand, DbCommand command, SqlConfiguration config) - { - if (Transaction != null) - { - command.Transaction = Transaction.GetDbTransaction(); - } - - command.CommandText = ParseSql(sqlCommand, config); - command.CommandType = CommandType.Text; - command.CommandTimeout = config.GetCommandTimeout(Context); - - if (config.Parameters == null) - { - return; - } - - var i = 0; - foreach (var t in config.Parameters) - { - var parameterName = string.IsNullOrEmpty(t.Name) ? $"Param{i}" : t.Name; - var parameter = command.CreateParameter(); - parameter.ParameterName = parameterName; - parameter.Value = t.Value; - command.Parameters.Add(parameter); - i++; - } - } - - /// - /// Gets the query with tag using the specified query - /// - /// The query - /// The tag - /// The string - private static string GetQueryWithTag(string query, string tag) - { - var queryBuilder = new StringBuilder(); - queryBuilder.AppendLine($"--{tag}"); - queryBuilder.AppendLine(); - queryBuilder.Append(query); - return queryBuilder.ToString(); - } - - private static SqlConfiguration GetConfig(Action config = null) - { - var configuration = new DefaultSqlConfiguration(); - config?.Invoke(configuration); - - return configuration; - } - - /// - /// Disposes this instance - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the disposing - /// - /// The disposing - protected virtual void Dispose(bool disposing) - { - if (Disposed) - { - return; - } - - if (disposing) - { - Transaction?.Dispose(); - Context?.Dispose(); - } - - Disposed = true; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs index 24c58d4..9e70c91 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/Repository/UnitOfWork.cs @@ -1,266 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.MySql.Repository; -public abstract class UnitOfWork : SqlExecutor -{ - protected static int IsMigrating; - - protected UnitOfWork(DbContext context) : base(context) - { - } - - internal DbContext GetDbContext() => Context; -} - -public abstract class UnitOfWork : UnitOfWork, ISqlUnitOfWork +/// +/// MySQL unit of work. The implementation lives in ; +/// MySQL uses the default ANSI CALL stored-procedure dialect, so no dialect override is +/// required. +/// +public abstract class UnitOfWork : RelationalUnitOfWork where TDbContext : DbContext { - private readonly TDbContext _context; - private readonly IServiceProvider _serviceProvider; - - protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) - { - _serviceProvider = serviceProvider; - _context = context; - } - - public int Commit() - { - return _context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = _context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await _context.SaveChangesAsync(cancellationToken); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await _context.SaveChangesAsync(cancellationToken); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).Attach(item); - } - - public IEnumerable GetPendingMigrations() - { - return _context.Database.GetPendingMigrations(); - } - - public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - ((Set)InternalCreateSet()).LoadProperty(item, selector); - } - - public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).LoadProperty(item, propertyName); - } - - public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); - } - - public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - _context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); - } - else - { - await _context.Entry(item).Collection(navigationProperty).LoadAsync(); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = _context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - _context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - _context.Entry(item).State = EntityState.Modified; - } - - public void UpdateDatabase() - { - if (0 != Interlocked.Exchange(ref IsMigrating, 1)) - { - return; - } - - try - { - _context.Database.Migrate(); - } - finally - { - Interlocked.Exchange(ref IsMigrating, 0); - } - } - - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork - { - var repo = _serviceProvider.GetRequiredService>(); - return repo; - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(serviceProvider, context) { - return _serviceProvider.GetRequiredService>(); } - - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => - new Set(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj index d283474..6de303c 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net10.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql - 10.0.2.0 - eQuantic Systems + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -13,35 +12,22 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 10.0.2.0 - 10.0.2.0 - Icon.png - latest + 10.1.0.0 + 10.1.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,8 +38,6 @@ - - @@ -64,6 +48,8 @@ + Include="..\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj" /> + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj index de585a9..9af0202 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net8.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and MySQL eQuantic.Core.Data.EntityFramework.MySql - 8.1.2.0 - eQuantic Systems + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.MySql eQuantic.Core.Data.EntityFramework.MySql @@ -13,35 +12,22 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 8.1.2.0 - 8.1.2.0 - Icon.png - latest + 8.2.0.0 + 8.2.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,8 +38,6 @@ - - @@ -64,6 +48,8 @@ + Include="..\eQuantic.Core.Data.EntityFramework\eQuantic.Core.Data.EntityFramework.csproj" /> + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj deleted file mode 100644 index 0df09ac..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.Net9.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Core Data library for Entity Framework and MySQL - eQuantic.Core.Data.EntityFramework.MySql - 9.1.2.0 - eQuantic Systems - net9.0 - eQuantic.Core.Data.EntityFramework.MySql - eQuantic.Core.Data.EntityFramework.MySql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 9.1.2.0 - 9.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj b/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj deleted file mode 100644 index 7b01e0c..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/eQuantic.Core.Data.EntityFramework.MySql.csproj +++ /dev/null @@ -1,104 +0,0 @@ - - - - - Core Data library for Entity Framework and MySQL - eQuantic.Core.Data.EntityFramework.MySql - 4.4.2.0 - eQuantic Systems - net8.0;net9.0;net10.0 - eQuantic.Core.Data.EntityFramework.MySql - eQuantic.Core.Data.EntityFramework.MySql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 4.4.2.0 - 4.4.2.0 - Icon.png - latest - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs deleted file mode 100644 index a121d43..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/ExpressionConverter.cs +++ /dev/null @@ -1,176 +0,0 @@ -#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.PostgreSql; - -internal class ExpressionConverter -{ - public static Expression, SetPropertyCalls>> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(SetPropertyCalls), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var block = methodCalls.Last(); - - return Expression.Lambda, SetPropertyCalls>>(block, parameter); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - var setPropertyMethod = typeof(SetPropertyCalls) - .GetMethods() - .Where(m => m.Name == nameof(SetPropertyCalls.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsGenericFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression.Reduce() : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsGenericFunc(Type type) - { - return type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Func<,>); - } - } -} -#endif -#if NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.PostgreSql; - -internal class ExpressionConverter -{ - public static Action> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(UpdateSettersBuilder), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var lastCall = methodCalls.Last(); - var body = lastCall.Type == typeof(void) - ? lastCall - : Expression.Block(typeof(void), lastCall); - return Expression.Lambda>>(body, parameter).Compile(); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - - var setPropertyMethod = typeof(UpdateSettersBuilder) - .GetMethods() - .Where(m => m.Name == nameof(UpdateSettersBuilder.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsExpressionFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsExpressionFunc(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Expression<>)) - { - var delegateType = type.GetGenericArguments()[0]; - - return delegateType.IsGenericType && - delegateType.GetGenericTypeDefinition() == typeof(Func<,>); - } - return false; - } - } -} -#endif \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs index 2c60b1d..5e09900 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Set.cs @@ -1,268 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +/// +/// PostgreSQL entity set. The implementation lives in ; this +/// type is preserved for source compatibility. +/// +public class Set : RelationalSet where TEntity : class, IEntity { public Set(DbContext context) : base(context) { } - - public override long DeleteMany(Expression> filter) - { - return InternalDbSet.Where(filter).ExecuteDelete(); - } - - public override async Task DeleteManyAsync(Expression> filter, - CancellationToken cancellationToken = default) - { - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); - } - - public void LoadCollection(TChildEntity item, - Expression>> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Collection(selector).Load(); - } - - public void LoadCollection(TChildEntity item, string propertyName) - where TChildEntity : class - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - - public async Task LoadCollectionAsync(TChildEntity item, - Expression>> selector) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Collection(selector).LoadAsync(); - } - - public async Task LoadCollectionAsync(TChildEntity item, string propertyName) - where TChildEntity : class - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(); - } - - public void LoadProperties(TEntity entity, params string[] properties) - { - if (properties is not { Length: > 0 }) - { - return; - } - - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - LoadProperty(entity, property); - } - else - { - LoadCascade(props, entity); - } - } - } - - public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) - { - if (properties is { Length: > 0 }) - { - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - await LoadPropertyAsync(entity, property); - } - else - { - await LoadCascadeAsync(props, entity); - } - } - } - } - - public void LoadProperty(TChildEntity item, - Expression> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Reference(selector).Load(); - } - - public void LoadProperty(TChildEntity item, string propertyName) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - else - { - DbContext.Entry(item).Reference(propertyName).Load(); - } - } - - public async Task LoadPropertyAsync(TChildEntity item, - Expression> selector, CancellationToken cancellationToken = default) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken); - } - - public async Task LoadPropertyAsync(TChildEntity item, string propertyName, - CancellationToken cancellationToken = default) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken); - } - else - { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken); - } - } - - public override long UpdateMany(Expression> filter, - Expression> updateExpression) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); - } - - public override async Task UpdateManyAsync(Expression> filter, - Expression> updateExpression, CancellationToken cancellationToken = default) - { - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken); - } - - private void LoadCascade(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - LoadProperty(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - LoadCascade(props, nextObj, index + 1); - } - } - - private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - await LoadPropertyAsync(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - await LoadCascadeAsync(props, nextObj, index + 1); - } - } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs index 3e03f66..92f5b7e 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/UnitOfWork.cs @@ -1,266 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; -public abstract class UnitOfWork : SqlExecutor -{ - protected static int IsMigrating; - - protected UnitOfWork(DbContext context) : base(context) - { - } - - internal DbContext GetDbContext() => Context; -} - -public abstract class UnitOfWork : UnitOfWork, ISqlUnitOfWork +/// +/// PostgreSQL unit of work. The implementation lives in +/// ; PostgreSQL uses the default ANSI CALL +/// stored-procedure dialect, so no dialect override is required. +/// +public abstract class UnitOfWork : RelationalUnitOfWork where TDbContext : DbContext { - private readonly TDbContext _context; - private readonly IServiceProvider _serviceProvider; - - protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) - { - _serviceProvider = serviceProvider; - _context = context; - } - - public int Commit() - { - return _context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = _context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await _context.SaveChangesAsync(cancellationToken); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await _context.SaveChangesAsync(cancellationToken); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).Attach(item); - } - - public IEnumerable GetPendingMigrations() - { - return _context.Database.GetPendingMigrations(); - } - - public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - ((Set)InternalCreateSet()).LoadProperty(item, selector); - } - - public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).LoadProperty(item, propertyName); - } - - public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); - } - - public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - _context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); - } - else - { - await _context.Entry(item).Collection(navigationProperty).LoadAsync(); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = _context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - _context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - _context.Entry(item).State = EntityState.Modified; - } - - public void UpdateDatabase() - { - if (0 != Interlocked.Exchange(ref IsMigrating, 1)) - { - return; - } - - try - { - _context.Database.Migrate(); - } - finally - { - Interlocked.Exchange(ref IsMigrating, 0); - } - } - - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork - { - var repo = _serviceProvider.GetRequiredService>(); - return repo; - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(serviceProvider, context) { - return _serviceProvider.GetRequiredService>(); } - - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => - new Set(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj index e54452c..ded86c3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net10.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql - 10.0.2.0 - eQuantic Systems + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -13,35 +12,22 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 10.0.2.0 - 10.0.2.0 - Icon.png - latest + 10.1.0.0 + 10.1.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,8 +38,6 @@ - - @@ -65,5 +49,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj index 445e528..faef4c3 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net8.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and PostgreSQL eQuantic.Core.Data.EntityFramework.PostgreSql - 8.1.2.0 - eQuantic Systems + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.PostgreSql eQuantic.Core.Data.EntityFramework.PostgreSql @@ -13,35 +12,22 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 8.1.2.0 - 8.1.2.0 - Icon.png - latest + 8.2.0.0 + 8.2.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,8 +38,6 @@ - - @@ -65,5 +49,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj deleted file mode 100644 index 09ce817..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.Net9.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Core Data library for Entity Framework and PostgreSQL - eQuantic.Core.Data.EntityFramework.PostgreSql - 9.1.2.0 - eQuantic Systems - net9.0 - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 9.1.2.0 - 9.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj b/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj deleted file mode 100644 index ffb4690..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/eQuantic.Core.Data.EntityFramework.PostgreSql.csproj +++ /dev/null @@ -1,102 +0,0 @@ - - - - - Core Data library for Entity Framework and PostgreSQL - eQuantic.Core.Data.EntityFramework.PostgreSql - 4.4.2.0 - eQuantic Systems - net8.0;net9.0;net10.0 - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic.Core.Data.EntityFramework.PostgreSql - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 4.4.2.0 - 4.4.2.0 - Icon.png - latest - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.MySql/ExpressionConverter.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/ExpressionConverter.cs similarity index 93% rename from src/eQuantic.Core.Data.EntityFramework.MySql/ExpressionConverter.cs rename to src/eQuantic.Core.Data.EntityFramework.Relational/ExpressionConverter.cs index 4e0c57a..bbde74d 100644 --- a/src/eQuantic.Core.Data.EntityFramework.MySql/ExpressionConverter.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/ExpressionConverter.cs @@ -1,4 +1,4 @@ -#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER +#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER using System; using System.Collections.Generic; using System.Linq; @@ -6,7 +6,7 @@ using System.Reflection; using Microsoft.EntityFrameworkCore.Query; -namespace eQuantic.Core.Data.EntityFramework.MySql; +namespace eQuantic.Core.Data.EntityFramework.Relational; internal class ExpressionConverter { @@ -18,7 +18,7 @@ public static Expression, SetPropertyCalls), "e"); var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); var block = methodCalls.Last(); - + return Expression.Lambda, SetPropertyCalls>>(block, parameter); } @@ -60,7 +60,7 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall parameters[1].ParameterType == genericArgs[0]; }) .MakeGenericMethod(propertyType); - + var setPropertyCall = Expression.Call( callExpression != null ? callExpression.Reduce() : parameter, setPropertyMethod, @@ -69,7 +69,7 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall ); _setPropertyCalls.Add(setPropertyCall); - + return setPropertyCall; } @@ -89,7 +89,7 @@ private static bool IsGenericFunc(Type type) using System.Reflection; using Microsoft.EntityFrameworkCore.Query; -namespace eQuantic.Core.Data.EntityFramework.MySql; +namespace eQuantic.Core.Data.EntityFramework.Relational; internal class ExpressionConverter { @@ -101,8 +101,8 @@ public static Action> ConvertExpression( var parameter = Expression.Parameter(typeof(UpdateSettersBuilder), "e"); var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); var lastCall = methodCalls.Last(); - var body = lastCall.Type == typeof(void) - ? lastCall + var body = lastCall.Type == typeof(void) + ? lastCall : Expression.Block(typeof(void), lastCall); return Expression.Lambda>>(body, parameter).Compile(); } @@ -129,25 +129,25 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall { var propertyInfo = (PropertyInfo)binding.Member; var propertyType = propertyInfo.PropertyType; - + var entityParameter = Expression.Parameter(typeof(T), "entity"); var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - + var setPropertyMethod = typeof(UpdateSettersBuilder) .GetMethods() .Where(m => m.Name == nameof(UpdateSettersBuilder.SetProperty) && m.IsGenericMethod) - .Single(m => + .Single(m => { var parameters = m.GetParameters(); var genericArgs = m.GetGenericArguments(); return genericArgs.Length == 1 && - parameters.Length == 2 && + parameters.Length == 2 && IsExpressionFunc(parameters[0].ParameterType) && parameters[1].ParameterType == genericArgs[0]; }) .MakeGenericMethod(propertyType); - + var setPropertyCall = Expression.Call( callExpression != null ? callExpression : parameter, setPropertyMethod, @@ -156,7 +156,7 @@ private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCall ); _setPropertyCalls.Add(setPropertyCall); - + return setPropertyCall; } @@ -165,12 +165,12 @@ private static bool IsExpressionFunc(Type type) if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Expression<>)) { var delegateType = type.GetGenericArguments()[0]; - - return delegateType.IsGenericType && + + return delegateType.IsGenericType && delegateType.GetGenericTypeDefinition() == typeof(Func<,>); } return false; } } } -#endif \ No newline at end of file +#endif diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..b5d5e97 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,51 @@ +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; +using eQuantic.Core.Data.EntityFramework.Repository.Options; +using eQuantic.Core.Data.Repository; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using BaseServiceCollectionExtensions = eQuantic.Core.Data.EntityFramework.Repository.Extensions.ServiceCollectionExtensions; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; + +/// +/// Relational registration helpers. These wrap the SQL-agnostic base registration and additionally +/// expose when the unit of work implements it. The base +/// AddQueryableRepositories no longer knows about because that +/// contract moved into the Relational provider in v5. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Registers the relational unit of work, the generic repositories and, when + /// implements it, . + /// + /// The unit of work interface. + /// The unit of work implementation. + /// The service collection. + /// Optional repository options (lifetime, assembly scanning). + /// The service collection for chaining. + public static IServiceCollection AddRelationalRepositories( + this IServiceCollection services, Action options = null) + where TUnitOfWorkInterface : IQueryableUnitOfWork + where TUnitOfWorkImpl : class, TUnitOfWorkInterface + { + options ??= _ => { }; + + BaseServiceCollectionExtensions.AddQueryableRepositories(services, options); + + var repoOptions = new RepositoryOptions(); + options(repoOptions); + var lifetime = repoOptions.GetLifetime(); + + // Only expose ISqlUnitOfWork when the implementation actually provides it. Registering it + // unconditionally would make resolving ISqlUnitOfWork throw for a non-SQL unit of work. + if (typeof(ISqlUnitOfWork).IsAssignableFrom(typeof(TUnitOfWorkImpl))) + { + services.TryAdd(new ServiceDescriptor(typeof(ISqlUnitOfWork), + sp => sp.GetRequiredService(), lifetime)); + } + + return services; + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs similarity index 64% rename from src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs rename to src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs index 98ba669..7ce7350 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/Extensions/SqlConfigurationExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/Extensions/SqlConfigurationExtensions.cs @@ -1,12 +1,12 @@ -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using Microsoft.EntityFrameworkCore; -namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository.Extensions; +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; -public static class SqlConfigurationExtensions +internal static class SqlConfigurationExtensions { private const int DefaultCommandTimeout = 60; - + public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) { return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs new file mode 100644 index 0000000..07e719e --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSet.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Repository; +using eQuantic.Core.Data.Repository; +using Microsoft.EntityFrameworkCore; +#if NET6_0 || NETSTANDARD2_1 +using Z.EntityFramework.Plus; +#endif + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; + +/// +/// The shared relational entity set used by the SqlServer, PostgreSql and MySql providers. +/// +public class RelationalSet : SetBase where TEntity : class, IEntity +{ + public RelationalSet(DbContext context) : base(context) + { + } + + public override long DeleteMany(Expression> filter) + { +#if NET6_0 || NETSTANDARD2_1 + return InternalDbSet.Where(filter).Delete(); +#else + return InternalDbSet.Where(filter).ExecuteDelete(); +#endif + } + + public override async Task DeleteManyAsync(Expression> filter, + CancellationToken cancellationToken = default) + { +#if NET6_0 || NETSTANDARD2_1 + return await InternalDbSet.Where(filter).DeleteAsync(cancellationToken).ConfigureAwait(false); +#else + return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); +#endif + } + + public void LoadCollection(TChildEntity item, + Expression>> selector) + where TChildEntity : class + where TComplexProperty : class + { + DbContext.Entry(item).Collection(selector).Load(); + } + + public void LoadCollection(TChildEntity item, string propertyName) + where TChildEntity : class + { + DbContext.Entry(item).Collection(propertyName).Load(); + } + + public async Task LoadCollectionAsync(TChildEntity item, + Expression>> selector) + where TChildEntity : class where TComplexProperty : class + { + await DbContext.Entry(item).Collection(selector).LoadAsync().ConfigureAwait(false); + } + + public async Task LoadCollectionAsync(TChildEntity item, string propertyName) + where TChildEntity : class + { + await DbContext.Entry(item).Collection(propertyName).LoadAsync().ConfigureAwait(false); + } + + public void LoadProperties(TEntity entity, params string[] properties) + { + if (properties is not { Length: > 0 }) + { + return; + } + + foreach (var property in properties) + { + if (string.IsNullOrEmpty(property)) + { + continue; + } + + var props = property.Split('.'); + + if (props.Length == 1) + { + LoadProperty(entity, property); + } + else + { + LoadCascade(props, entity); + } + } + } + + public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) + { + if (properties is { Length: > 0 }) + { + foreach (var property in properties) + { + if (string.IsNullOrEmpty(property)) + { + continue; + } + + var props = property.Split('.'); + + if (props.Length == 1) + { + await LoadPropertyAsync(entity, property).ConfigureAwait(false); + } + else + { + await LoadCascadeAsync(props, entity).ConfigureAwait(false); + } + } + } + } + + public void LoadProperty(TChildEntity item, + Expression> selector) + where TChildEntity : class + where TComplexProperty : class + { + DbContext.Entry(item).Reference(selector).Load(); + } + + public void LoadProperty(TChildEntity item, string propertyName) + where TChildEntity : class + { + if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) + { + DbContext.Entry(item).Collection(propertyName).Load(); + } + else + { + DbContext.Entry(item).Reference(propertyName).Load(); + } + } + + public async Task LoadPropertyAsync(TChildEntity item, + Expression> selector, CancellationToken cancellationToken = default) + where TChildEntity : class where TComplexProperty : class + { + await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task LoadPropertyAsync(TChildEntity item, string propertyName, + CancellationToken cancellationToken = default) + where TChildEntity : class + { + if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) + { + await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); + } + else + { + await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken).ConfigureAwait(false); + } + } + + public override long UpdateMany(Expression> filter, + Expression> updateExpression) + { +#if NET6_0 || NETSTANDARD2_1 + return InternalDbSet.Where(filter).Update(updateExpression); +#else + var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); + return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); +#endif + } + + public override async Task UpdateManyAsync(Expression> filter, + Expression> updateExpression, CancellationToken cancellationToken = default) + { +#if NET6_0 || NETSTANDARD2_1 + return await InternalDbSet.Where(filter).UpdateAsync(updateExpression, cancellationToken).ConfigureAwait(false); +#else + var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); + return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken).ConfigureAwait(false); +#endif + } + + private void LoadCascade(string[] props, object obj, int index = 0) + { + if (obj == null) + { + return; + } + + var prop = obj.GetType().GetProperty(props[index]); + var nextObj = prop?.GetValue(obj); + if (nextObj == null) + { + LoadProperty(obj, props[index]); + nextObj = prop?.GetValue(obj); + } + + if (props.Length > index + 1) + { + LoadCascade(props, nextObj, index + 1); + } + } + + private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) + { + if (obj == null) + { + return; + } + + var prop = obj.GetType().GetProperty(props[index]); + var nextObj = prop?.GetValue(obj); + if (nextObj == null) + { + await LoadPropertyAsync(obj, props[index]).ConfigureAwait(false); + nextObj = prop?.GetValue(obj); + } + + if (props.Length > index + 1) + { + await LoadCascadeAsync(props, nextObj, index + 1).ConfigureAwait(false); + } + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs similarity index 76% rename from src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs rename to src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs index 25ec1a0..059aebc 100644 --- a/src/eQuantic.Core.Data.EntityFramework.PostgreSql/Repository/SqlExecutor.cs +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalSqlExecutor.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -7,22 +7,23 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.PostgreSql.Repository.Extensions; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; +using eQuantic.Core.Data.EntityFramework.Relational.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -namespace eQuantic.Core.Data.EntityFramework.PostgreSql.Repository; +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; /// -/// The sql executor class +/// The shared relational sql executor. Providers reuse this implementation and only override the +/// dialect-specific when needed (e.g. SQL Server uses EXEC +/// whereas PostgreSQL/MySQL use the ANSI CALL). /// /// /// /// [ExcludeFromCodeCoverage] -public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable +public abstract class RelationalSqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable { /// /// The context @@ -38,12 +39,12 @@ public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable /// The transaction /// protected IDbContextTransaction Transaction; - + /// /// Initializes a new instance of the class /// /// The context - protected SqlExecutor(DbContext context) + protected RelationalSqlExecutor(DbContext context) { Context = context; } @@ -55,7 +56,7 @@ protected SqlExecutor(DbContext context) public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) { Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken); + Transaction = await Context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); } /// @@ -66,10 +67,10 @@ public virtual async Task CommitTransactionAsync(CancellationToken cancellationT { if (Transaction != null) { - await Transaction.CommitAsync(cancellationToken); + await Transaction.CommitAsync(cancellationToken).ConfigureAwait(false); } } - + /// /// Rollbacks the transaction using the specified cancellation token /// @@ -78,24 +79,24 @@ public async Task RollbackTransactionAsync(CancellationToken cancellationToken = { if (Transaction != null) { - await Transaction.RollbackAsync(cancellationToken); + await Transaction.RollbackAsync(cancellationToken).ConfigureAwait(false); } } - + /// public async Task> ExecuteRawSqlAsync(string sql, Func map, Action config = null, CancellationToken cancellationToken = default) { var configuration = GetConfig(config); - + await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sql, command, configuration); - await Context.Database.OpenConnectionAsync(cancellationToken); - await using var result = await command.ExecuteReaderAsync(cancellationToken); + await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); + await using var result = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); var items = new List(); - while (await result.ReadAsync(cancellationToken)) + while (await result.ReadAsync(cancellationToken).ConfigureAwait(false)) { items.Add(map(result)); } @@ -131,7 +132,7 @@ public TResult ExecuteFunction(string name, Action() - .FromSqlRaw(sql) + .FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefault(); } @@ -144,7 +145,7 @@ public TResult ExecuteFunction(string name, Action config = null) { var configuration = GetConfig(config); - + using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); @@ -153,7 +154,7 @@ public int ExecuteCommand(string sqlCommand, Action con return result == DBNull.Value ? 0 : Convert.ToInt32(result); } - + /// /// Executes the command using the specified command timeout /// @@ -165,12 +166,12 @@ public async Task ExecuteCommandAsync(string sqlCommand, Action config = null, CancellationToken cancellationToken = default) { var configuration = GetConfig(config); - + await using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sqlCommand, command, configuration); - await Context.Database.OpenConnectionAsync(cancellationToken); - var result = await command.ExecuteScalarAsync(cancellationToken); + await Context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); + var result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false); return result == DBNull.Value ? 0 : Convert.ToInt32(result); } @@ -183,16 +184,16 @@ public async Task ExecuteCommandAsync(string sqlCommand, /// /// The cancellation token /// A task containing the result - public Task ExecuteFunctionAsync(string name, + public Task ExecuteFunctionAsync(string name, Action config = null, CancellationToken cancellationToken = default) where TResult : class { var configuration = GetConfig(config); var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql) + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)) .FirstOrDefaultAsync(cancellationToken); } - + /// /// Executes the procedure using the specified name /// @@ -202,9 +203,9 @@ public Task ExecuteFunctionAsync(string name, public int ExecuteProcedure(string name, Action config = null) { var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";"); + return ExecuteCommand(BuildProcedureSql(name, configuration) + ";", config); } - + /// /// Executes the query using the specified sql query /// @@ -216,9 +217,9 @@ public IEnumerable ExecuteQuery(string sqlQuery, Action().FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value)); + return Context.Set().FromSqlRaw(sql, GetParameterValues(configuration)); } - + /// /// Executes the transaction using the specified operation /// @@ -247,7 +248,7 @@ public Task ExecuteProcedureAsync(string name, Action @@ -293,7 +294,7 @@ public IEnumerable ExecuteRawSql(string sql, Func map, Action config = null) { var configuration = GetConfig(config); - + using var command = Context.Database.GetDbConnection().CreateCommand(); SetCommand(sql, command, configuration); @@ -308,7 +309,7 @@ public IEnumerable ExecuteRawSql(string sql, Func map, return items; } - + /// /// Gets the transaction /// @@ -334,7 +335,7 @@ public void UseTransaction(DbTransaction transaction) { Context.Database.UseTransaction(transaction); } - + /// /// Uses the transaction using the specified transaction /// @@ -344,35 +345,66 @@ public Task UseTransactionAsync(DbTransaction transaction, CancellationToken can { return Context.Database.UseTransactionAsync(transaction, cancellationToken); } - + /// - /// Gets the query function using the specified name + /// Gets the query function using the specified name. Parameter values are emitted as positional + /// placeholders ({0}, {1}, …) so the values travel as + /// s through FromSqlRaw and are never + /// interpolated into the SQL text. /// /// The name /// The configuration. /// The string - private static string GetQueryFunction(string name, SqlConfiguration config) + internal static string GetQueryFunction(string name, SqlConfiguration config) { - return $"SELECT {name}({GetQueryParameters(config.Parameters.ToArray())} )"; + return $"SELECT {name}({GetPositionalPlaceholders(config.Parameters.Count)} )"; } - + /// - /// Gets the query procedure using the specified name + /// Builds the stored-procedure invocation. The default is the ANSI CALL name(args) form; + /// SQL Server overrides this with EXEC name args. Parameter values are emitted as named + /// placeholders matching the s created by , so + /// the values are never interpolated into the SQL text. /// - /// The name + /// The procedure name. /// The configuration. - /// The string - private static string GetQueryProcedure(string name, SqlConfiguration config) + /// The SQL text. + internal virtual string BuildProcedureSql(string name, SqlConfiguration config) { - return $"EXEC {name}{GetQueryParameters(config.Parameters.ToArray())}"; + return $"CALL {name}({GetNamedPlaceholders(config.Parameters.ToArray())} )"; } - + /// - /// Gets the query parameters using the specified parameters + /// Builds a comma-separated list of positional placeholders ({0}, {1}, …) for the + /// given parameter count. Used by FromSqlRaw, which substitutes each placeholder with a + /// parameter reference. + /// + /// The number of parameters. + /// The placeholder string. + internal static string GetPositionalPlaceholders(int count) + { + var cmdBuilder = new StringBuilder(); + for (var i = 0; i < count; i++) + { + if (i > 0) + { + cmdBuilder.Append(','); + } + + cmdBuilder.Append(" {").Append(i).Append('}'); + } + + return cmdBuilder.ToString(); + } + + /// + /// Builds a comma-separated list of named placeholders (@Param0, @Name, …) using + /// the same naming convention as , so the placeholders bind to the + /// parameters added to the command. /// /// The parameters - /// The string - private static string GetQueryParameters(params ParamValue[] parameters) + /// The placeholder string. + internal static string GetNamedPlaceholders(params ParamValue[] parameters) { var cmdBuilder = new StringBuilder(); if (parameters is not { Length: > 0 }) @@ -387,29 +419,30 @@ private static string GetQueryParameters(params ParamValue[] parameters) cmdBuilder.Append(','); } - if (parameters[i].Value == null) - { - cmdBuilder.Append(" NULL"); - } - else - { - var fmt = " {0}"; - if (parameters[i].Value is Guid or string or DateTime) - { - fmt = " '{0}'"; - } - - cmdBuilder.Append(string.Format(fmt, parameters[i].Value)); - } + var parameterName = string.IsNullOrEmpty(parameters[i].Name) ? $"Param{i}" : parameters[i].Name; + cmdBuilder.Append(" @").Append(parameterName); } return cmdBuilder.ToString(); } + /// + /// Gets the ordered parameter values used to feed FromSqlRaw's positional placeholders. + /// + /// The configuration. + /// The parameter values, in the same order as the emitted placeholders. + internal static object[] GetParameterValues(SqlConfiguration config) + { + return config.Parameters == null + ? Array.Empty() + : config.Parameters.Select(p => p.Value).ToArray(); + } + private static string ParseSql(string sql, SqlConfiguration config) { return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; } + /// /// Sets the command using the specified command timeout /// @@ -458,15 +491,15 @@ private static string GetQueryWithTag(string query, string tag) queryBuilder.Append(query); return queryBuilder.ToString(); } - - private static SqlConfiguration GetConfig(Action config = null) + + private static DefaultSqlConfiguration GetConfig(Action config = null) { var configuration = new DefaultSqlConfiguration(); config?.Invoke(configuration); return configuration; } - + /// /// Disposes this instance /// @@ -475,7 +508,7 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } - + /// /// Disposes the disposing /// diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs new file mode 100644 index 0000000..d17f2ec --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Repository/RelationalUnitOfWork.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Repository; + +/// +/// The shared non-generic relational unit of work. Providers derive their own thin +/// UnitOfWork<TDbContext> from . +/// +public abstract class RelationalUnitOfWork : RelationalSqlExecutor +{ + protected static int IsMigrating; + + protected RelationalUnitOfWork(DbContext context) : base(context) + { + } + + internal DbContext GetDbContext() => Context; +} + +public abstract class RelationalUnitOfWork : RelationalUnitOfWork, ISqlUnitOfWork + where TDbContext : DbContext +{ + private readonly TDbContext _context; + private readonly IServiceProvider _serviceProvider; + + protected RelationalUnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) + { + _serviceProvider = serviceProvider; + _context = context; + } + + public int Commit() + { + return _context.SaveChanges(); + } + + public int CommitAndRefreshChanges() + { + var changes = 0; + var saveFailed = false; + + do + { + try + { + changes = _context.SaveChanges(); + + saveFailed = false; + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + ex.Entries.ToList() + .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); + } + } while (saveFailed); + + return changes; + } + + public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) + { + var changes = 0; + var saveFailed = false; + + do + { + try + { + changes = await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + + saveFailed = false; + } + catch (DbUpdateConcurrencyException ex) + { + saveFailed = true; + + ex.Entries.ToList() + .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); + } + } while (saveFailed); + + return changes; + } + + public async Task CommitAsync(CancellationToken cancellationToken = default) + { + return await _context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + + public int Commit(Action options) + { + return Commit(); + } + + public int CommitAndRefreshChanges(Action options) + { + return CommitAndRefreshChanges(); + } + + public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) + { + return CommitAndRefreshChangesAsync(cancellationToken); + } + + public Task CommitAsync(Action options, CancellationToken cancellationToken = default) + { + return CommitAsync(cancellationToken); + } + + public Data.Repository.ISet CreateSet() where TEntity : class, IEntity => + InternalCreateSet(); + + public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity + { + ((RelationalSet)InternalCreateSet()).ApplyCurrentValues(original, current); + } + + public void Attach(TEntity item) where TEntity : class, IEntity + { + ((RelationalSet)InternalCreateSet()).Attach(item); + } + + public IEnumerable GetPendingMigrations() + { + return _context.Database.GetPendingMigrations(); + } + + public void LoadProperty(TEntity item, Expression> selector) + where TEntity : class, IEntity + where TComplexProperty : class + { + ((RelationalSet)InternalCreateSet()).LoadProperty(item, selector); + } + + public void LoadProperty(TEntity item, string propertyName) + where TEntity : class, IEntity + { + ((RelationalSet)InternalCreateSet()).LoadProperty(item, propertyName); + } + + public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) + where TEntity : class, IEntity + where TComplexProperty : class + { + return ((RelationalSet)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); + } + + public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) + where TEntity : class, IEntity + { + return ((RelationalSet)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); + } + + public void LoadCollection(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class + { + if (filter != null) + { + _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); + } + else + { + _context.Entry(item).Collection(navigationProperty).Load(); + } + } + + public async Task LoadCollectionAsync(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class + { + if (filter != null) + { + await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync().ConfigureAwait(false); + } + else + { + await _context.Entry(item).Collection(navigationProperty).LoadAsync().ConfigureAwait(false); + } + } + + public void Reload(TEntity item) where TEntity : class + { + var entry = _context.Entry(item); + entry.CurrentValues.SetValues(entry.OriginalValues); + entry.Reload(); + } + + public void RollbackChanges() + { + // set all entities in change tracker + // as 'unchanged state' + _context?.ChangeTracker.Entries() + .ToList() + .ForEach(entry => entry.State = EntityState.Unchanged); + } + + public void SetModified(TEntity item) where TEntity : class + { + //this operation also attach item in object state manager + _context.Entry(item).State = EntityState.Modified; + } + + public void UpdateDatabase() + { + if (0 != Interlocked.Exchange(ref IsMigrating, 1)) + { + return; + } + + try + { + _context.Database.Migrate(); + } + finally + { + Interlocked.Exchange(ref IsMigrating, 0); + } + } + + public virtual SaveOptions GetSaveOptions() + { + return new SaveOptions(); + } + + public virtual IRepository GetRepository() + where TEntity : class, IEntity + { + return _serviceProvider.GetRequiredService>(); + } + + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity + { + return _serviceProvider.GetRequiredService>(); + } + + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity + { + return _serviceProvider.GetRequiredService>(); + } + + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity + { + return _serviceProvider.GetRequiredService>(); + } + + private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity => + new RelationalSet(_context); +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs new file mode 100644 index 0000000..e3213fc --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/IAsyncSqlExecutor.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// The async sql executor interface +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public interface IAsyncSqlExecutor where TConfig : SqlConfiguration +{ + /// + /// Begins the transaction asynchronous. + /// + /// The cancellation token. + /// + Task BeginTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Commits the transaction asynchronous. + /// + /// The cancellation token. + /// + Task CommitTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Rollbacks the transaction asynchronous. + /// + /// The cancellation token. + /// + Task RollbackTransactionAsync(CancellationToken cancellationToken = default); + + /// + /// Executes the raw SQL asynchronous. + /// + /// + /// The SQL. + /// The map. + /// The configuration. + /// The cancellation token. + /// + Task> ExecuteRawSqlAsync(string sql, Func map, Action config = null, CancellationToken cancellationToken = default); + + /// + /// Execute async arbitrary command into underlying persistence store + /// + /// Command to execute + /// + /// The cancellation token. + /// The number of affected records + Task ExecuteCommandAsync(string sqlCommand, Action config = null, CancellationToken cancellationToken = default); + + /// + /// Execute Function Async. + /// + /// + /// + /// + /// The cancellation token. + /// + Task ExecuteFunctionAsync(string name, Action config = null, CancellationToken cancellationToken = default) + where TResult : class; + + /// + /// Execute Procedure Async. + /// + /// + /// + /// The cancellation token. + /// + Task ExecuteProcedureAsync(string name, Action config = null, + CancellationToken cancellationToken = default); + + /// + /// Use transaction async. + /// + /// + /// The cancellation token. + /// + Task UseTransactionAsync(DbTransaction transaction, CancellationToken cancellationToken = default); +} + +public interface IAsyncSqlExecutor : IAsyncSqlExecutor +{ +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs new file mode 100644 index 0000000..9327179 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlExecutor.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// Base contract for support 'dialect specific queries'. +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public interface ISqlExecutor where TConfig : SqlConfiguration +{ + /// + /// Begins the transaction. + /// + void BeginTransaction(); + + /// + /// Commits the transaction. + /// + void CommitTransaction(); + + /// + /// Executes the raw SQL. + /// + /// + /// The SQL. + /// The map. + /// The configuration. + /// + IEnumerable ExecuteRawSql(string sql, Func map, Action config = null); + + /// + /// Execute arbitrary command into underlying persistence store + /// + /// + /// Command to execute + /// + /// SELECT idCustomer,Name FROM dbo.[Customers] WHERE idCustomer > {0} + /// + /// + /// The configuration a vector of parameters values + /// The number of affected records + int ExecuteCommand(string sqlCommand, Action config = null); + + /// + /// Executes a SQL statement using the specified sql command + /// + /// The sql command + /// + /// The number of rows affected. + public int ExecuteNonQuery(string sqlCommand, Action config = null); + + /// + /// + /// + /// + /// + /// + /// + TResult ExecuteFunction(string name, Action config = null) where TResult : class; + + /// + /// + /// + /// + /// + /// + int ExecuteProcedure(string name, Action config = null); + + /// + /// Execute specific query with underlying persistence store + /// + /// Entity type to map query results + /// + /// Dialect Query + /// + /// SELECT idCustomer,Name FROM dbo.[Customers] WHERE idCustomer > {0} + /// + /// + /// The configuration with a vector of parameters values + /// + /// Enumerable results + /// + IEnumerable ExecuteQuery(string sqlQuery, Action config = null) where TEntity : class; + + /// + /// Executes the transaction. + /// + /// The operation. + void ExecuteTransaction(Action operation); + + /// + /// Executes the transaction async. + /// + /// The operation. + /// The cancellation token. + Task ExecuteTransactionAsync(Func operation, CancellationToken cancellationToken = default); + + /// + /// Get transaction. + /// + /// + DbTransaction GetTransaction(); + + /// + /// Rollbacks the transaction. + /// + void RollbackTransaction(); + + /// + /// Use transaction. + /// + /// + void UseTransaction(DbTransaction transaction); +} + +public interface ISqlExecutor : ISqlExecutor +{ +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs new file mode 100644 index 0000000..c9de201 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ISqlUnitOfWork.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// The UnitOfWork contract for the EF relational implementation. +/// +/// This contract extends IQueryableUnitOfWork with relational/SQL specific operations. +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). The new() entity constraint that v4 required +/// has been removed to match the v5 surface. +/// +/// +public interface ISqlUnitOfWork : IQueryableUnitOfWork, ISqlExecutor, IAsyncSqlExecutor +{ + /// + /// Apply current values in + /// + /// The type of entity + /// The original entity + /// The current entity + void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity; + + /// + /// Attach this item into "ObjectStateManager" + /// + /// The type of entity + /// The item + void Attach(TEntity item) where TEntity : class, IEntity; + + /// + /// + /// + /// + IEnumerable GetPendingMigrations(); + + /// + /// + /// + /// + /// + /// + /// + /// + void LoadCollection(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) + where TEntity : class + where TElement : class; + + /// + /// + /// + /// + /// + /// + /// + /// + /// + Task LoadCollectionAsync(TEntity item, + Expression>> navigationProperty, + Expression> filter = null) where TEntity : class where TElement : class; + + /// + /// + /// + /// + /// + /// + /// + void LoadProperty(TEntity item, Expression> selector) + where TEntity : class, IEntity + where TComplexProperty : class; + + /// + /// + /// + /// + /// + /// + void LoadProperty(TEntity item, string propertyName) + where TEntity : class, IEntity; + + /// + /// + /// + /// + /// + /// + /// + /// The cancellation token + /// + Task LoadPropertyAsync(TEntity item, + Expression> selector, CancellationToken cancellationToken = default) + where TEntity : class, IEntity + where TComplexProperty : class; + + /// + /// + /// + /// + /// + /// + /// The cancellation token. + /// + Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) + where TEntity : class, IEntity; + + /// + /// Reload this item ignoring cache + /// + /// + /// + void Reload(TEntity item) where TEntity : class; + + /// + /// Set object as modified + /// + /// The type of entity + /// The entity item to set as modifed + void SetModified(TEntity item) where TEntity : class; + + /// + /// + /// + void UpdateDatabase(); +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs new file mode 100644 index 0000000..759e670 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/ParamValue.cs @@ -0,0 +1,43 @@ +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// A named (or positional) parameter value carried through the relational SQL executor. Values +/// flow into commands as s and are never interpolated +/// into the SQL text. +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Sql +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public sealed class ParamValue +{ + public string Name { get; } + public object Value { get; } + + private ParamValue(string name, object value) : this(value) + { + Name = name; + } + + private ParamValue(object value) + { + Value = value; + } + + public static ParamValue Create(string name, object value) => new ParamValue(name, value); + public static ParamValue Create(object value) => new ParamValue(value); + + public override bool Equals(object obj) + { + return obj is ParamValue paramValue && ( + !string.IsNullOrEmpty(paramValue.Name) + ? paramValue.Name == Name + : paramValue.Name == Name && paramValue.Value.Equals(Value) + ); + } + + public override int GetHashCode() + { + return (Name, Value).GetHashCode(); + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs new file mode 100644 index 0000000..4fc2cb6 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/Sql/SqlConfiguration.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace eQuantic.Core.Data.EntityFramework.Relational.Sql; + +/// +/// Configuration for a raw SQL / stored-procedure / function invocation performed through the +/// relational SQL executor. +/// +/// +/// Rehomed into the Relational provider from the removed eQuantic.Core.Data.Repository.Config +/// namespace (dropped in eQuantic.Core.Data v5). +/// +public class SqlConfiguration +{ + public HashSet Parameters { get; protected set; } = new(); + public string Tag { get; protected set; } + public int? CommandTimeout { get; protected set; } +} + +public class SqlConfiguration : SqlConfiguration where TConfig : SqlConfiguration +{ + public TConfig WithParameters(params object[] parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + Parameters = new HashSet(parameters.Select(ParamValue.Create)); + return (TConfig)this; + } + + public TConfig WithParameters(params ParamValue[] parameters) + { + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + + Parameters.UnionWith(parameters); + return (TConfig)this; + } + + public TConfig WithTag(string tag) + { + Tag = tag ?? throw new ArgumentNullException(nameof(tag)); + return (TConfig)this; + } + + public TConfig WithCommandTimeout(int commandTimeout) + { + CommandTimeout = commandTimeout; + return (TConfig)this; + } +} + +public class DefaultSqlConfiguration : SqlConfiguration +{ +} diff --git a/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj new file mode 100644 index 0000000..772d6c0 --- /dev/null +++ b/src/eQuantic.Core.Data.EntityFramework.Relational/eQuantic.Core.Data.EntityFramework.Relational.csproj @@ -0,0 +1,54 @@ + + + + + Shared relational implementation for eQuantic Core Data Entity Framework providers + eQuantic.Core.Data.EntityFramework.Relational + 4.0.0.0 + net8.0;net10.0 + eQuantic.Core.Data.EntityFramework.Relational + eQuantic.Core.Data.EntityFramework.Relational + eQuantic;Core;Data;Library;Repository;Pattern;SQL;Relational + Shared relational base used by the SqlServer, PostgreSql and MySql providers + + 4.0.0.0 + 4.0.0.0 + + + + + + + + + + + + + + + + + + + + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.SqlServer + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.PostgreSql + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.MySql + + + <_Parameter1>eQuantic.Core.Data.EntityFramework.SqlServer.Tests + + + + + + + + diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs deleted file mode 100644 index 5ce05bf..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/ExpressionConverter.cs +++ /dev/null @@ -1,176 +0,0 @@ -#if NET7_0_OR_GREATER && !NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer; - -internal class ExpressionConverter -{ - public static Expression, SetPropertyCalls>> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(SetPropertyCalls), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var block = methodCalls.Last(); - - return Expression.Lambda, SetPropertyCalls>>(block, parameter); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - var setPropertyMethod = typeof(SetPropertyCalls) - .GetMethods() - .Where(m => m.Name == nameof(SetPropertyCalls.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsGenericFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression.Reduce() : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsGenericFunc(Type type) - { - return type.IsGenericType && - type.GetGenericTypeDefinition() == typeof(Func<,>); - } - } -} -#endif -#if NET10_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Microsoft.EntityFrameworkCore.Query; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer; - -internal class ExpressionConverter -{ - public static Action> ConvertExpression( - Expression> updateExpression) - { - ArgumentNullException.ThrowIfNull(updateExpression); - - var parameter = Expression.Parameter(typeof(UpdateSettersBuilder), "e"); - var methodCalls = new ExpressionRewriter(parameter).Rewrite(updateExpression.Body); - var lastCall = methodCalls.Last(); - var body = lastCall.Type == typeof(void) - ? lastCall - : Expression.Block(typeof(void), lastCall); - return Expression.Lambda>>(body, parameter).Compile(); - } - - private class ExpressionRewriter(ParameterExpression parameter) : ExpressionVisitor - { - private readonly List _setPropertyCalls = []; - - public IEnumerable Rewrite(Expression body) - { - if (body is not MemberInitExpression memberInit) - throw new NotSupportedException("Only MemberInit expressions are supported."); - - MethodCallExpression currentExpression = null; - foreach (var binding in memberInit.Bindings.OfType()) - { - currentExpression = RewriteBinding(binding, currentExpression); - } - - return _setPropertyCalls; - } - - private MethodCallExpression RewriteBinding(MemberAssignment binding, MethodCallExpression callExpression = null) - { - var propertyInfo = (PropertyInfo)binding.Member; - var propertyType = propertyInfo.PropertyType; - - var entityParameter = Expression.Parameter(typeof(T), "entity"); - var propertyAccess = Expression.MakeMemberAccess(entityParameter, propertyInfo); - var propertyLambda = Expression.Lambda(propertyAccess, entityParameter); - - var setPropertyMethod = typeof(UpdateSettersBuilder) - .GetMethods() - .Where(m => m.Name == nameof(UpdateSettersBuilder.SetProperty) && m.IsGenericMethod) - .Single(m => - { - var parameters = m.GetParameters(); - var genericArgs = m.GetGenericArguments(); - return genericArgs.Length == 1 && - parameters.Length == 2 && - IsExpressionFunc(parameters[0].ParameterType) && - parameters[1].ParameterType == genericArgs[0]; - }) - .MakeGenericMethod(propertyType); - - var setPropertyCall = Expression.Call( - callExpression != null ? callExpression : parameter, - setPropertyMethod, - propertyLambda, - binding.Expression - ); - - _setPropertyCalls.Add(setPropertyCall); - - return setPropertyCall; - } - - private static bool IsExpressionFunc(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Expression<>)) - { - var delegateType = type.GetGenericArguments()[0]; - - return delegateType.IsGenericType && - delegateType.GetGenericTypeDefinition() == typeof(Func<,>); - } - return false; - } - } -} -#endif \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs deleted file mode 100644 index 429b446..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Extensions/SqlConfigurationExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using eQuantic.Core.Data.Repository.Config; -using Microsoft.EntityFrameworkCore; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository.Extensions; - -public static class SqlConfigurationExtensions -{ - private const int DefaultCommandTimeout = 60; - - public static int GetCommandTimeout(this SqlConfiguration config, DbContext context) - { - return config?.CommandTimeout ?? context?.Database.GetCommandTimeout() ?? DefaultCommandTimeout; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs index aeb413f..88a9aa8 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/Set.cs @@ -1,287 +1,16 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.Repository; -using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Linq.Extensions; using Microsoft.EntityFrameworkCore; -#if NET6_0 || NETSTANDARD2_1 -using Z.EntityFramework.Plus; -#endif namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; -public class Set : SetBase where TEntity : class, IEntity, new() +/// +/// SQL Server entity set. The implementation lives in ; this +/// type is preserved for source compatibility. +/// +public class Set : RelationalSet where TEntity : class, IEntity { public Set(DbContext context) : base(context) { } - - public override long DeleteMany(Expression> filter) - { -#if NET6_0 || NETSTANDARD2_1 - return InternalDbSet.Where(filter).Delete(); -#else - return InternalDbSet.Where(filter).ExecuteDelete(); -#endif - } - - public override async Task DeleteManyAsync(Expression> filter, - CancellationToken cancellationToken = default) - { -#if NET6_0 || NETSTANDARD2_1 - return await InternalDbSet.Where(filter).DeleteAsync(cancellationToken); -#else - return await InternalDbSet.Where(filter).ExecuteDeleteAsync(cancellationToken); -#endif - } - - public void LoadCollection(TChildEntity item, - Expression>> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Collection(selector).Load(); - } - - public void LoadCollection(TChildEntity item, string propertyName) - where TChildEntity : class - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - - public async Task LoadCollectionAsync(TChildEntity item, - Expression>> selector) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Collection(selector).LoadAsync(); - } - - public async Task LoadCollectionAsync(TChildEntity item, string propertyName) - where TChildEntity : class - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(); - } - - public void LoadProperties(TEntity entity, params string[] properties) - { - if (properties is not { Length: > 0 }) - { - return; - } - - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - LoadProperty(entity, property); - } - else - { - LoadCascade(props, entity); - } - } - } - - public async Task LoadPropertiesAsync(TEntity entity, params string[] properties) - { - if (properties is { Length: > 0 }) - { - foreach (var property in properties) - { - if (string.IsNullOrEmpty(property)) - { - continue; - } - - var props = property.Split('.'); - - if (props.Length == 1) - { - await LoadPropertyAsync(entity, property); - } - else - { - await LoadCascadeAsync(props, entity); - } - } - } - } - - public void LoadProperty(TChildEntity item, - Expression> selector) - where TChildEntity : class - where TComplexProperty : class - { - DbContext.Entry(item).Reference(selector).Load(); - } - - public void LoadProperty(TChildEntity item, string propertyName) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - DbContext.Entry(item).Collection(propertyName).Load(); - } - else - { - DbContext.Entry(item).Reference(propertyName).Load(); - } - } - - public async Task LoadPropertyAsync(TChildEntity item, - Expression> selector, CancellationToken cancellationToken = default) - where TChildEntity : class where TComplexProperty : class - { - await DbContext.Entry(item).Reference(selector).LoadAsync(cancellationToken); - } - - public async Task LoadPropertyAsync(TChildEntity item, string propertyName, - CancellationToken cancellationToken = default) - where TChildEntity : class - { - if (typeof(IEnumerable).IsAssignableFrom(typeof(TChildEntity).GetProperty(propertyName)!.PropertyType)) - { - await DbContext.Entry(item).Collection(propertyName).LoadAsync(cancellationToken); - } - else - { - await DbContext.Entry(item).Reference(propertyName).LoadAsync(cancellationToken); - } - } - - public override long UpdateMany(Expression> filter, - Expression> updateExpression) - { -#if NET6_0 || NETSTANDARD2_1 - return InternalDbSet.Where(filter).Update(updateExpression); -#else - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return InternalDbSet.Where(filter).ExecuteUpdate(convertedExpression); -#endif - } - - public override async Task UpdateManyAsync(Expression> filter, - Expression> updateExpression, CancellationToken cancellationToken = default) - { -#if NET6_0 || NETSTANDARD2_1 - return await InternalDbSet.Where(filter).UpdateAsync(updateExpression, cancellationToken); -#else - var convertedExpression = ExpressionConverter.ConvertExpression(updateExpression); - return await InternalDbSet.Where(filter).ExecuteUpdateAsync(convertedExpression, cancellationToken); -#endif - } - - private void LoadCascade(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - LoadProperty(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - LoadCascade(props, nextObj, index + 1); - } - } - - private async Task LoadCascadeAsync(string[] props, object obj, int index = 0) - { - if (obj == null) - { - return; - } - - var prop = obj.GetType().GetProperty(props[index]); - var nextObj = prop?.GetValue(obj); - if (nextObj == null) - { - await LoadPropertyAsync(obj, props[index]); - nextObj = prop?.GetValue(obj); - } - - if (props.Length > index + 1) - { - await LoadCascadeAsync(props, nextObj, index + 1); - } - } - - internal Expression> GetExpression(TKey id) - { - return DbContext.GetFindByKeyExpression(id); - } - - public override IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - { - if (configuration == null) - { - return internalQueryAction.Invoke(this); - } - - var config = GetConfig(configuration); - var queryableConfig = config as QueryableConfiguration; - - var query = string.IsNullOrEmpty(queryableConfig?.SqlRaw) ? this : InternalDbSet.FromSqlRaw(queryableConfig.SqlRaw); - - if (config.HasNoTracking) - { - query = query.AsNoTracking(); - } - - if (config.Properties?.Any() == true) - { - query = query.IncludeMany(config.Properties.ToArray()); - } - - if (queryableConfig?.IgnoreQueryFilters == true) - { - query = query.IgnoreQueryFilters(); - } - - if (!string.IsNullOrEmpty(config.Tag)) - { - query = query.TagWith(config.Tag); - } - - if (queryableConfig != null) - { - query = queryableConfig.BeforeCustomization.Invoke(query); - } - - query = internalQueryAction.Invoke(query); - - if (config.SortingColumns.Any()) - { - query = query.OrderBy(config.SortingColumns.ToArray()); - } - - if (queryableConfig != null) - { - query = queryableConfig.AfterCustomization.Invoke(query); - } - - return query; - } } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs deleted file mode 100644 index d6b9473..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/SqlExecutor.cs +++ /dev/null @@ -1,498 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.EntityFramework.SqlServer.Repository.Extensions; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Sql; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; - -namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; - -/// -/// The sql executor class -/// -/// -/// -/// -[ExcludeFromCodeCoverage] -public abstract class SqlExecutor : ISqlExecutor, IAsyncSqlExecutor, IDisposable -{ - /// - /// The context - /// - protected readonly DbContext Context; - - /// - /// The disposed - /// - protected bool Disposed; - - /// - /// The transaction - /// - protected IDbContextTransaction Transaction; - - /// - /// Initializes a new instance of the class - /// - /// The context - protected SqlExecutor(DbContext context) - { - Context = context; - } - - /// - /// Begins the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task BeginTransactionAsync(CancellationToken cancellationToken = default) - { - Transaction?.Dispose(); - Transaction = await Context.Database.BeginTransactionAsync(cancellationToken); - } - - /// - /// Commits the transaction using the specified cancellation token - /// - /// The cancellation token - public virtual async Task CommitTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.CommitAsync(cancellationToken); - } - } - - /// - /// Rollbacks the transaction using the specified cancellation token - /// - /// The cancellation token - public async Task RollbackTransactionAsync(CancellationToken cancellationToken = default) - { - if (Transaction != null) - { - await Transaction.RollbackAsync(cancellationToken); - } - } - - /// - public async Task> ExecuteRawSqlAsync(string sql, Func map, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken); - await using var result = await command.ExecuteReaderAsync(cancellationToken); - - var items = new List(); - while (await result.ReadAsync(cancellationToken)) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Executes the non query using the specified sql command - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteNonQuery(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - return command.ExecuteNonQuery(); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// The configuration. - /// The result - public TResult ExecuteFunction(string name, Action config = null) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set() - .FromSqlRaw(sql) - .FirstOrDefault(); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// The configuration. - /// The int - public int ExecuteCommand(string sqlCommand, Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - Context.Database.OpenConnection(); - var result = command.ExecuteScalar(); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the command using the specified command timeout - /// - /// The sql command - /// - /// The cancellation token - /// A task containing the int - public async Task ExecuteCommandAsync(string sqlCommand, - Action config = null, CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - - await using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sqlCommand, command, configuration); - - await Context.Database.OpenConnectionAsync(cancellationToken); - var result = await command.ExecuteScalarAsync(cancellationToken); - - return result == DBNull.Value ? 0 : Convert.ToInt32(result); - } - - /// - /// Executes the function using the specified name - /// - /// The result - /// The name - /// - /// The cancellation token - /// A task containing the result - public Task ExecuteFunctionAsync(string name, - Action config = null, - CancellationToken cancellationToken = default) where TResult : class - { - var configuration = GetConfig(config); - var sql = ParseSql(GetQueryFunction(name, configuration), configuration); - return Context.Set().FromSqlRaw(sql) - .FirstOrDefaultAsync(cancellationToken); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// The configuration - /// The int - public int ExecuteProcedure(string name, Action config = null) - { - var configuration = GetConfig(config); - return ExecuteCommand(GetQueryProcedure(name, configuration) + ";"); - } - - /// - /// Executes the query using the specified sql query - /// - /// The entity - /// The sql query - /// The configuration. - /// An enumerable of t entity - public IEnumerable ExecuteQuery(string sqlQuery, Action config = null) where TEntity : class - { - var configuration = GetConfig(config); - var sql = ParseSql(sqlQuery, configuration); - return Context.Set().FromSqlRaw(sql, configuration.Parameters.Select(p => p.Value)); - } - - /// - /// Executes the transaction using the specified operation - /// - /// The operation - /// - public void ExecuteTransaction(Action operation) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - strategy.Execute(() => { operation.Invoke((ISqlUnitOfWork)this); }); - } - - /// - /// Executes the procedure using the specified name - /// - /// The name - /// - /// The cancellation token - /// A task containing the int - public Task ExecuteProcedureAsync(string name, Action config = null, - CancellationToken cancellationToken = default) - { - var configuration = GetConfig(config); - return ExecuteCommandAsync(GetQueryProcedure(name, configuration) + ";", config, cancellationToken); - } - - /// - public Task ExecuteTransactionAsync(Func operation, - CancellationToken cancellationToken = default) - { - if (operation == null) - { - throw new ArgumentNullException(nameof(operation)); - } - - var strategy = Context.Database.CreateExecutionStrategy(); - - return strategy.ExecuteAsync(() => operation.Invoke((ISqlUnitOfWork)this)); - } - - /// - /// Begins the transaction - /// - public void BeginTransaction() - { - Transaction?.Dispose(); - Transaction = Context.Database.BeginTransaction(); - } - - /// - /// Commits the transaction - /// - public virtual void CommitTransaction() - { - Transaction?.Commit(); - } - - /// - /// Executes the raw sql using the specified sql - /// - /// The - /// The sql - /// The map - /// The configuration. - /// The items - public IEnumerable ExecuteRawSql(string sql, Func map, - Action config = null) - { - var configuration = GetConfig(config); - - using var command = Context.Database.GetDbConnection().CreateCommand(); - SetCommand(sql, command, configuration); - - Context.Database.OpenConnection(); - using var result = command.ExecuteReader(); - - var items = new List(); - while (result.Read()) - { - items.Add(map(result)); - } - - return items; - } - - /// - /// Gets the transaction - /// - /// The db transaction - public DbTransaction GetTransaction() - { - return Transaction?.GetDbTransaction(); - } - - /// - /// Rollbacks the transaction - /// - public void RollbackTransaction() - { - Transaction?.Rollback(); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - public void UseTransaction(DbTransaction transaction) - { - Context.Database.UseTransaction(transaction); - } - - /// - /// Uses the transaction using the specified transaction - /// - /// The transaction - /// The cancellation token - public Task UseTransactionAsync(DbTransaction transaction, CancellationToken cancellationToken = default) - { - return Context.Database.UseTransactionAsync(transaction, cancellationToken); - } - - /// - /// Gets the query function using the specified name - /// - /// The name - /// The configuration. - /// The string - private static string GetQueryFunction(string name, SqlConfiguration config) - { - return $"SELECT {name}({GetQueryParameters(config.Parameters.ToArray())} )"; - } - - /// - /// Gets the query procedure using the specified name - /// - /// The name - /// The configuration. - /// The string - private static string GetQueryProcedure(string name, SqlConfiguration config) - { - return $"EXEC {name}{GetQueryParameters(config.Parameters.ToArray())}"; - } - - /// - /// Gets the query parameters using the specified parameters - /// - /// The parameters - /// The string - private static string GetQueryParameters(params ParamValue[] parameters) - { - var cmdBuilder = new StringBuilder(); - if (parameters is not { Length: > 0 }) - { - return cmdBuilder.ToString(); - } - - for (var i = 0; i < parameters.Length; i++) - { - if (i > 0) - { - cmdBuilder.Append(','); - } - - if (parameters[i].Value == null) - { - cmdBuilder.Append(" NULL"); - } - else - { - var fmt = " {0}"; - if (parameters[i].Value is Guid or string or DateTime) - { - fmt = " '{0}'"; - } - - cmdBuilder.Append(string.Format(fmt, parameters[i].Value)); - } - } - - return cmdBuilder.ToString(); - } - - private static string ParseSql(string sql, SqlConfiguration config) - { - return !string.IsNullOrEmpty(config.Tag) ? GetQueryWithTag(sql, config.Tag) : sql; - } - /// - /// Sets the command using the specified command timeout - /// - /// The sql command - /// The command - /// The configuration. - private void SetCommand(string sqlCommand, DbCommand command, SqlConfiguration config) - { - if (Transaction != null) - { - command.Transaction = Transaction.GetDbTransaction(); - } - - command.CommandText = ParseSql(sqlCommand, config); - command.CommandType = CommandType.Text; - command.CommandTimeout = config.GetCommandTimeout(Context); - - if (config.Parameters == null) - { - return; - } - - var i = 0; - foreach (var t in config.Parameters) - { - var parameterName = string.IsNullOrEmpty(t.Name) ? $"Param{i}" : t.Name; - var parameter = command.CreateParameter(); - parameter.ParameterName = parameterName; - parameter.Value = t.Value; - command.Parameters.Add(parameter); - i++; - } - } - - /// - /// Gets the query with tag using the specified query - /// - /// The query - /// The tag - /// The string - private static string GetQueryWithTag(string query, string tag) - { - var queryBuilder = new StringBuilder(); - queryBuilder.AppendLine($"--{tag}"); - queryBuilder.AppendLine(); - queryBuilder.Append(query); - return queryBuilder.ToString(); - } - - private static SqlConfiguration GetConfig(Action config = null) - { - var configuration = new DefaultSqlConfiguration(); - config?.Invoke(configuration); - - return configuration; - } - - /// - /// Disposes this instance - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes the disposing - /// - /// The disposing - protected virtual void Dispose(bool disposing) - { - if (Disposed) - { - return; - } - - if (disposing) - { - Transaction?.Dispose(); - Context?.Dispose(); - } - - Disposed = true; - } -} diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs index fcfa394..2ce2170 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Repository/UnitOfWork.cs @@ -1,266 +1,25 @@ -using System; -using System.Collections.Generic; +using System; using System.Linq; -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; -using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Options; -using eQuantic.Core.Data.Repository.Sql; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Repository; -public abstract class UnitOfWork : SqlExecutor -{ - protected static int IsMigrating; - - protected UnitOfWork(DbContext context) : base(context) - { - } - - internal DbContext GetDbContext() => Context; -} - -public abstract class UnitOfWork : UnitOfWork, ISqlUnitOfWork +/// +/// SQL Server unit of work. The implementation lives in +/// ; this type only supplies the SQL Server dialect +/// (stored procedures are invoked with EXEC rather than the ANSI CALL). +/// +public abstract class UnitOfWork : RelationalUnitOfWork where TDbContext : DbContext { - private readonly TDbContext _context; - private readonly IServiceProvider _serviceProvider; - - protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(context) - { - _serviceProvider = serviceProvider; - _context = context; - } - - public int Commit() - { - return _context.SaveChanges(); - } - - public int CommitAndRefreshChanges() - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = _context.SaveChanges(); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) - { - var changes = 0; - var saveFailed = false; - - do - { - try - { - changes = await _context.SaveChangesAsync(cancellationToken); - - saveFailed = false; - } - catch (DbUpdateConcurrencyException ex) - { - saveFailed = true; - - ex.Entries.ToList() - .ForEach(entry => entry.OriginalValues.SetValues(entry.GetDatabaseValues())); - } - } while (saveFailed); - - return changes; - } - - public async Task CommitAsync(CancellationToken cancellationToken = default) - { - return await _context.SaveChangesAsync(cancellationToken); - } - - public int Commit(Action options) - { - return Commit(); - } - - public int CommitAndRefreshChanges(Action options) - { - return CommitAndRefreshChanges(); - } - - public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAndRefreshChangesAsync(cancellationToken); - } - - public Task CommitAsync(Action options, CancellationToken cancellationToken = default) - { - return CommitAsync(cancellationToken); - } - - Data.Repository.ISet IQueryableUnitOfWork.CreateSet() => InternalCreateSet(); - - public void ApplyCurrentValues(TEntity original, TEntity current) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).ApplyCurrentValues(original, current); - } - - public void Attach(TEntity item) where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).Attach(item); - } - - public IEnumerable GetPendingMigrations() - { - return _context.Database.GetPendingMigrations(); - } - - public void LoadProperty(TEntity item, Expression> selector) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - ((Set)InternalCreateSet()).LoadProperty(item, selector); - } - - public void LoadProperty(TEntity item, string propertyName) - where TEntity : class, IEntity, new() - { - ((Set)InternalCreateSet()).LoadProperty(item, propertyName); - } - - public Task LoadPropertyAsync(TEntity item, Expression> selector, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - where TComplexProperty : class - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, selector, cancellationToken); - } - - public Task LoadPropertyAsync(TEntity item, string propertyName, CancellationToken cancellationToken = default) - where TEntity : class, IEntity, new() - { - return ((Set)InternalCreateSet()).LoadPropertyAsync(item, propertyName, cancellationToken); - } - - public void LoadCollection(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - _context.Entry(item).Collection(navigationProperty).Query().Where(filter).Load(); - } - else - { - _context.Entry(item).Collection(navigationProperty).Load(); - } - } - - public async Task LoadCollectionAsync(TEntity item, - Expression>> navigationProperty, - Expression> filter = null) where TEntity : class where TElement : class - { - if (filter != null) - { - await _context.Entry(item).Collection(navigationProperty).Query().Where(filter).LoadAsync(); - } - else - { - await _context.Entry(item).Collection(navigationProperty).LoadAsync(); - } - } - - public void Reload(TEntity item) where TEntity : class - { - var entry = _context.Entry(item); - entry.CurrentValues.SetValues(entry.OriginalValues); - entry.Reload(); - } - - public void RollbackChanges() - { - // set all entities in change tracker - // as 'unchanged state' - _context?.ChangeTracker.Entries() - .ToList() - .ForEach(entry => entry.State = EntityState.Unchanged); - } - - public void SetModified(TEntity item) where TEntity : class - { - //this operation also attach item in object state manager - _context.Entry(item).State = EntityState.Modified; - } - - public void UpdateDatabase() - { - if (0 != Interlocked.Exchange(ref IsMigrating, 1)) - { - return; - } - - try - { - _context.Database.Migrate(); - } - finally - { - Interlocked.Exchange(ref IsMigrating, 0); - } - } - - public virtual Data.Repository.ISet CreateSet() where TEntity : class, IEntity, new() => - InternalCreateSet(); - - public virtual SaveOptions GetSaveOptions() - { - return new SaveOptions(); - } - - public virtual IRepository GetRepository() - where TEntity : class, IEntity, new() where TUnitOfWork : IUnitOfWork - { - var repo = _serviceProvider.GetRequiredService>(); - return repo; - } - - public IAsyncRepository GetAsyncRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IUnitOfWork - { - return _serviceProvider.GetRequiredService>(); - } - - public IQueryableRepository GetQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + protected UnitOfWork(IServiceProvider serviceProvider, TDbContext context) : base(serviceProvider, context) { - return _serviceProvider.GetRequiredService>(); } - public IAsyncQueryableRepository GetAsyncQueryableRepository() - where TEntity : class, IEntity, new() - where TUnitOfWork : IQueryableUnitOfWork + internal override string BuildProcedureSql(string name, SqlConfiguration config) { - return _serviceProvider.GetRequiredService>(); + return $"EXEC {name}{GetNamedPlaceholders(config.Parameters.ToArray())}"; } - - private Data.Repository.ISet InternalCreateSet() where TEntity : class, IEntity, new() => - new Set(_context); } diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs index ae79002..8517415 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/Specifications/GetEntityByIdSpecification.cs @@ -1,19 +1,19 @@ using System; using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; using eQuantic.Core.Data.EntityFramework.Repository.Extensions; -using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; using eQuantic.Core.Data.Repository; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Specifications; public class GetEntityByIdSpecification : Specification - where TEntity : class, IEntity, new() + where TEntity : class, IEntity { private readonly TKey _id; - private readonly UnitOfWork _unitOfWork; + private readonly RelationalUnitOfWork _unitOfWork; - public GetEntityByIdSpecification(TKey id, UnitOfWork unitOfWork) + public GetEntityByIdSpecification(TKey id, RelationalUnitOfWork unitOfWork) { _id = id; _unitOfWork = unitOfWork; @@ -22,4 +22,4 @@ public override Expression> SatisfiedBy() { return _unitOfWork.GetDbContext().GetFindByKeyExpression(_id); } -} \ No newline at end of file +} diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj index d3e4fbb..44fd947 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net10.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer - 10.0.2.0 - eQuantic Systems + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,35 +12,22 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 10.0.2.0 - 10.0.2.0 - Icon.png - latest + 10.1.0.0 + 10.1.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + Version="10.0.10" /> + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,8 +38,6 @@ - - @@ -65,5 +49,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj deleted file mode 100644 index c749e4c..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net6.csproj +++ /dev/null @@ -1,70 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 6.1.2.0 - eQuantic Systems - net6.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 6.1.2.0 - 6.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj deleted file mode 100644 index d522b0d..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net7.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 7.1.2.0 - eQuantic Systems - net7.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 7.1.2.0 - 7.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj index 9b6aa64..a4c2c46 100644 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net8.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework and SQL Server eQuantic.Core.Data.EntityFramework.SqlServer - 8.1.2.0 - eQuantic Systems + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework.SqlServer eQuantic.Core.Data.EntityFramework.SqlServer @@ -13,35 +12,22 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 8.1.2.0 - 8.1.2.0 - Icon.png - latest + 8.2.0.0 + 8.2.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + Version="8.0.29" /> + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,8 +38,6 @@ - - @@ -65,5 +49,7 @@ + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj deleted file mode 100644 index 0bad350..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.Net9.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 9.1.2.0 - eQuantic Systems - net9.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 9.1.2.0 - 9.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj b/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj deleted file mode 100644 index 1e9ade2..0000000 --- a/src/eQuantic.Core.Data.EntityFramework.SqlServer/eQuantic.Core.Data.EntityFramework.SqlServer.csproj +++ /dev/null @@ -1,149 +0,0 @@ - - - - - Core Data library for Entity Framework and SQL Server - eQuantic.Core.Data.EntityFramework.SqlServer - 4.4.2.0 - eQuantic Systems - net6.0;net7.0;net8.0;net9.0;net10.0 - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic.Core.Data.EntityFramework.SqlServer - eQuantic;Core;Data;Library;Repository;Pattern;SQL - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 4.4.2.0 - 4.4.2.0 - Icon.png - latest - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - - - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/.msbump b/src/eQuantic.Core.Data.EntityFramework/.msbump deleted file mode 100644 index 6850641..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/.msbump +++ /dev/null @@ -1,13 +0,0 @@ -{ - Configurations: { - "Debug": { - BumpLabel: "dev", - LabelDigits: 4 - }, - - "Release": { - BumpRevision: true, - ResetLabel: "dev" - } - } -} \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs index be7a1b7..b034016 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/AsyncQueryableRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; @@ -7,769 +7,136 @@ using eQuantic.Core.Data.EntityFramework.Repository.Read; using eQuantic.Core.Data.EntityFramework.Repository.Write; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Read; -using eQuantic.Core.Data.Repository.Write; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.Repository; [ExcludeFromCodeCoverage] -public class AsyncQueryableRepository : - QueryableRepository, - IAsyncRepository, TEntity, TKey>, - IAsyncQueryableRepository - where TUnitOfWork : class, IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class AsyncQueryableRepository : + AsyncQueryableReadRepository, + IAsyncQueryableRepository, + IQueryableRepository, + IAsyncRepository, + IRepository + where TEntity : class, IEntity { - private readonly IAsyncQueryableReadRepository _asyncReadRepository; - private readonly IAsyncWriteRepository _asyncWriteRepository; - private bool _disposed; + private readonly AsyncWriteRepository _asyncWriteRepository; - public AsyncQueryableRepository(TUnitOfWork unitOfWork) : base(unitOfWork) + public AsyncQueryableRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { - var asyncReadRepository = new AsyncQueryableReadRepository(unitOfWork); - asyncReadRepository.OwnUnitOfWork = false; - this._asyncReadRepository = asyncReadRepository; - - var asyncWriteRepository = new AsyncWriteRepository(unitOfWork); - asyncWriteRepository.OwnUnitOfWork = false; - this._asyncWriteRepository = asyncWriteRepository; + _asyncWriteRepository = new AsyncWriteRepository(unitOfWork); } - public Task AddAsync(TEntity item) - { - return this._asyncWriteRepository.AddAsync(item); - } + // -------------------------------------------------------------- synchronous write (IWriteRepository) - public Task> AllMatchingAsync( - ISpecification specification, - Action> configuration = default) + public void Add(TEntity item) { - return this._asyncReadRepository.AllMatchingAsync(specification, configuration); + _asyncWriteRepository.Add(item); } - public Task> AllMatchingAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllMatchingAsync(specification, configuration, cancellationToken); - } - public Task> AllMatchingAsync( - ISpecification specification, - CancellationToken cancellationToken) + public void AddRange(IEnumerable items) { - return this._asyncReadRepository.AllMatchingAsync(specification, cancellationToken); - } - - public Task CountAsync(CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.CountAsync(cancellationToken); + _asyncWriteRepository.AddRange(items); } - public Task CountAsync( - ISpecification specification, - CancellationToken cancellationToken = default) + public long DeleteMany(Expression> filter) { - return this._asyncReadRepository.CountAsync(specification, cancellationToken); + return _asyncWriteRepository.DeleteMany(filter); } - public Task CountAsync( - Expression> filter, - CancellationToken cancellationToken = default) + public long DeleteMany(ISpecification specification) { - return this._asyncReadRepository.CountAsync(filter, cancellationToken); + return _asyncWriteRepository.DeleteMany(specification); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public void Merge(TEntity persisted, TEntity current) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Merge(persisted, current); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public void Modify(TEntity item) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Modify(item); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public void Remove(TEntity item) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.Remove(item); } - public Task SumAsync(Expression> source) + public void TrackItem(TEntity item) { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + _asyncWriteRepository.TrackItem(item); } - public Task SumAsync(Expression> source) + public long UpdateMany(Expression> filter, Expression> updateFactory) { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + return _asyncWriteRepository.UpdateMany(filter, updateFactory); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public long UpdateMany(ISpecification specification, Expression> updateFactory) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + return _asyncWriteRepository.UpdateMany(specification, updateFactory); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); - } + // -------------------------------------------------------------- asynchronous write (IAsyncWriteRepository) - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public Task AddAsync(TEntity item, CancellationToken cancellationToken = default) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + return _asyncWriteRepository.AddAsync(item, cancellationToken); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) + public Task AddRangeAsync(IEnumerable items, CancellationToken cancellationToken = default) { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); + return _asyncWriteRepository.AddRangeAsync(items, cancellationToken); } - public Task SumAsync(Expression> source) - { - return _asyncReadRepository.SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return _asyncReadRepository.SumAsync(specification, source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return _asyncReadRepository.SumAsync(filter, source); - } - - public Task AllAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.AllAsync(specification, configuration); - } - - public Task AllAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(specification, configuration, cancellationToken); - } - - public Task AllAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(specification, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.AllAsync(filter, configuration); - } - - public Task AllAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(filter, configuration, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AllAsync(filter, cancellationToken); - } - - public Task AnyAsync( - Action> configuration = default, - CancellationToken cancellationToken = default) - { - return this._asyncReadRepository.AnyAsync(configuration, cancellationToken); - } - - public Task AnyAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.AnyAsync(specification, configuration); - } - - public Task AnyAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AnyAsync(specification, configuration, cancellationToken); - } - - public Task AnyAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AnyAsync(specification, cancellationToken); - } - - public Task AnyAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.AnyAsync(filter, configuration); - } - - public Task AnyAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AnyAsync(filter, configuration, cancellationToken); - } - - public Task AnyAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.AnyAsync(filter, cancellationToken); - } - - public Task DeleteManyAsync( - Expression> filter, + public Task DeleteManyAsync(Expression> filter, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.DeleteManyAsync(filter, cancellationToken); - } - - public Task DeleteManyAsync( - ISpecification specification, - CancellationToken cancellationToken = default) - { - return this._asyncWriteRepository.DeleteManyAsync(specification, cancellationToken); - } - - public Task> GetAllAsync( - Action> configuration = default) - { - return this._asyncReadRepository.GetAllAsync(configuration); - } - - public Task> GetAllAsync( - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAllAsync(configuration, cancellationToken); - } - - public Task> GetAllAsync( - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAllAsync(cancellationToken); - } - - public Task GetAsync( - TKey id, - Action> configuration = default) - { - return this._asyncReadRepository.GetAsync(id, configuration); - } - - public Task GetAsync( - TKey id, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAsync(id, configuration, cancellationToken); - } - - public Task GetAsync( - TKey id, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetAsync(id, cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetMappedAsync(filter, map, configuration); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(filter, map, configuration, cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(filter, map, cancellationToken); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetMappedAsync(specification, map, configuration); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(specification, map, configuration, cancellationToken); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetMappedAsync(specification, map, cancellationToken); + return _asyncWriteRepository.DeleteManyAsync(filter, cancellationToken); } - public Task> GetFilteredAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.GetFilteredAsync(filter, configuration); - } - - public Task> GetFilteredAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFilteredAsync(filter, configuration, cancellationToken); - } - - public Task> GetFilteredAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFilteredAsync(filter, cancellationToken); - } - - public Task GetFirstAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstAsync(filter, configuration); - } - - public Task GetFirstAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstAsync(filter, configuration, cancellationToken); - } - - public Task GetFirstAsync( - Expression> filter, + public Task DeleteManyAsync(ISpecification specification, CancellationToken cancellationToken = default) { - return this._asyncReadRepository.GetFirstAsync(filter, cancellationToken); - } - - public Task GetFirstAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstAsync(specification, configuration); - } - - public Task GetFirstAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstAsync(specification, configuration, cancellationToken); - } - - public Task GetFirstAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstAsync(specification, cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstMappedAsync(filter, map, configuration); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(filter, map, configuration, cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(filter, map, cancellationToken); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) - { - return this._asyncReadRepository.GetFirstMappedAsync(specification, map, configuration); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(specification, map, configuration, cancellationToken); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetFirstMappedAsync(specification, map, cancellationToken); - } - - public Task> GetPagedAsync( - int limit, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(limit, configuration); - } - - public Task> GetPagedAsync( - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - int limit, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(limit, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(specification, limit, configuration); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, limit, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(filter, limit, configuration); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, limit, cancellationToken); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(pageIndex, pageSize, configuration); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(pageIndex, pageSize, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(pageIndex, pageSize, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(specification, pageIndex, pageSize, configuration); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, pageIndex, pageSize, configuration, - cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(specification, pageIndex, pageSize, - cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration = default) - { - return this._asyncReadRepository.GetPagedAsync(filter, pageIndex, pageSize, configuration); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, pageIndex, pageSize, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetPagedAsync(filter, pageIndex, pageSize, cancellationToken); - } - - public Task GetSingleAsync( - Expression> filter, - Action> configuration = default) - { - return this._asyncReadRepository.GetSingleAsync(filter, configuration); - } - - public Task GetSingleAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(filter, configuration, cancellationToken); - } - - public Task GetSingleAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(filter, cancellationToken); - } - - public Task GetSingleAsync( - ISpecification specification, - Action> configuration = default) - { - return this._asyncReadRepository.GetSingleAsync(specification, configuration); - } - - public Task GetSingleAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(specification, configuration, cancellationToken); - } - - public Task GetSingleAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return this._asyncReadRepository.GetSingleAsync(specification, cancellationToken); + return _asyncWriteRepository.DeleteManyAsync(specification, cancellationToken); } public Task MergeAsync(TEntity persisted, TEntity current) { - return this._asyncWriteRepository.MergeAsync(persisted, current); + return _asyncWriteRepository.MergeAsync(persisted, current); } public Task ModifyAsync(TEntity item) { - return this._asyncWriteRepository.ModifyAsync(item); + return _asyncWriteRepository.ModifyAsync(item); } public Task RemoveAsync(TEntity item) { - return this._asyncWriteRepository.RemoveAsync(item); + return _asyncWriteRepository.RemoveAsync(item); } public Task UpdateManyAsync(Expression> filter, Expression> updateFactory, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.UpdateManyAsync(filter, updateFactory, cancellationToken); + return _asyncWriteRepository.UpdateManyAsync(filter, updateFactory, cancellationToken); } public Task UpdateManyAsync(ISpecification specification, Expression> updateFactory, CancellationToken cancellationToken = default) { - return this._asyncWriteRepository.UpdateManyAsync(specification, updateFactory, cancellationToken); + return _asyncWriteRepository.UpdateManyAsync(specification, updateFactory, cancellationToken); } protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (_disposed) - { - return; - } - if (disposing) { - this._asyncReadRepository?.Dispose(); - this._asyncWriteRepository?.Dispose(); - UnitOfWork?.Dispose(); + _asyncWriteRepository?.Dispose(); } - _disposed = true; + base.Dispose(disposing); } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs index 1812ffa..9c40125 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/DbContextExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; @@ -7,10 +9,23 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Extensions; public static class DbContextExtensions { + private static readonly ConcurrentDictionary<(Type Context, Type Entity), KeyProperty[]> KeyCache = new(); + + /// + /// Builds a predicate that matches an entity by its primary key. + /// + /// + /// The key value is referenced through a closure holder rather than embedded as a literal + /// , so EF Core parameterizes the query. Embedding the value + /// produced a distinct compiled-query cache entry (and a non-parameterized SQL literal) for + /// every id. The primary-key metadata is cached per (context type, entity type) to avoid the + /// model lookup on every call, and is used + /// so shadow keys (without a CLR property) are supported. + /// public static Expression> GetFindByKeyExpression(this DbContext dbContext, TKey id) { - var keyProperties = dbContext.Model.FindEntityType(typeof(TEntity))?.FindPrimaryKey()?.Properties; - if (keyProperties == null || !keyProperties.Any()) + var keyProperties = GetKeyProperties(dbContext); + if (keyProperties.Length == 0) { return null; } @@ -18,41 +33,157 @@ public static Expression> GetFindByKeyExpression>(expression, parameter); } -} \ No newline at end of file + + /// + /// Ensures a deterministic order before paging. If the query is already ordered, it is returned + /// unchanged; otherwise it is ordered by the primary key. Skip/Take without an OrderBy produces + /// non-deterministic pages (rows may repeat or vanish between pages) and warns in EF Core. + /// + public static IQueryable OrderByPrimaryKeyIfUnordered(this IQueryable query, DbContext dbContext) + where TEntity : class + { + if (IsOrdered(query.Expression)) + { + return query; + } + + var keyProperties = GetKeyProperties(dbContext); + if (keyProperties.Length == 0) + { + // No primary key to fall back on (e.g. a keyless entity); leave ordering to the caller. + return query; + } + + var ordered = query; + for (var i = 0; i < keyProperties.Length; i++) + { + var keyProperty = keyProperties[i]; + var parameter = Expression.Parameter(typeof(TEntity), "entity"); + var access = BuildPropertyAccess(parameter, keyProperty); + var selector = Expression.Lambda(access, parameter); + + var methodName = i == 0 ? nameof(Queryable.OrderBy) : nameof(Queryable.ThenBy); + var method = typeof(Queryable).GetMethods() + .First(m => m.Name == methodName && m.GetParameters().Length == 2) + .MakeGenericMethod(typeof(TEntity), keyProperty.ClrType); + ordered = (IQueryable)method.Invoke(null, new object[] { ordered, selector })!; + } + + return ordered; + } + + private static bool IsOrdered(Expression expression) + { + var detector = new OrderingDetector(); + detector.Visit(expression); + return detector.Found; + } + + private static KeyProperty[] GetKeyProperties(DbContext dbContext) + { + return KeyCache.GetOrAdd((dbContext.GetType(), typeof(TEntity)), _ => + { + var properties = dbContext.Model.FindEntityType(typeof(TEntity))?.FindPrimaryKey()?.Properties; + return properties == null + ? Array.Empty() + : properties.Select(p => new KeyProperty(p.Name, p.ClrType)).ToArray(); + }); + } + + private static MethodCallExpression BuildPropertyAccess(ParameterExpression parameter, KeyProperty keyProperty) + { + // EF.Property(entity, name) reads both mapped and shadow properties. + return Expression.Call( + typeof(EF), + nameof(EF.Property), + new[] { keyProperty.ClrType }, + parameter, + Expression.Constant(keyProperty.Name)); + } + + private static Expression BuildKeyValueAccess(TKey id) + { + // Wrapping the value in a holder and reading its field reproduces the closure pattern the C# + // compiler emits for `x => x.Id == id`, which EF Core parameterizes. + var holder = new KeyValueHolder(id); + return Expression.Field(Expression.Constant(holder), nameof(KeyValueHolder.Value)); + } + + private static Expression EnsureType(Expression expression, Type targetType) + { + return expression.Type == targetType ? expression : Expression.Convert(expression, targetType); + } + + private sealed class OrderingDetector : ExpressionVisitor + { + private static readonly HashSet OrderingMethods = new() + { + nameof(Queryable.OrderBy), nameof(Queryable.OrderByDescending), + nameof(Queryable.ThenBy), nameof(Queryable.ThenByDescending) + }; + + public bool Found { get; private set; } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(Queryable) && OrderingMethods.Contains(node.Method.Name)) + { + Found = true; + } + + return base.VisitMethodCall(node); + } + } + + private readonly struct KeyProperty + { + public KeyProperty(string name, Type clrType) + { + Name = name; + ClrType = clrType; + } + + public string Name { get; } + public Type ClrType { get; } + } + + private sealed class KeyValueHolder + { + public readonly T Value; + + public KeyValueHolder(T value) + { + Value = value; + } + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs index d90f8da..ec918c4 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/QueryableExtensions.cs @@ -1,6 +1,9 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Web; using Microsoft.EntityFrameworkCore; namespace eQuantic.Core.Data.EntityFramework.Repository.Extensions; @@ -48,4 +51,130 @@ public static IQueryable IncludeMany(this IQueryable return query; } + + /// + /// Applies the ordered set of sortings to the query, translating each + /// into the matching OrderBy/OrderByDescending/ThenBy/ + /// ThenByDescending call so the ordering stays server-side (EF translatable). + /// + /// The type of the entity. + /// The query. + /// The sortings, in application order. + /// The ordered query. + public static IQueryable ApplySorts(this IQueryable query, + IReadOnlyList> sortings) + { + if (sortings is not { Count: > 0 }) + { + return query; + } + + var first = true; + foreach (var sort in sortings) + { + if (sort == null) + { + continue; + } + + var method = first + ? (sort.Direction == SortDirection.Ascending ? nameof(Queryable.OrderBy) : nameof(Queryable.OrderByDescending)) + : (sort.Direction == SortDirection.Ascending ? nameof(Queryable.ThenBy) : nameof(Queryable.ThenByDescending)); + + var call = Expression.Call( + typeof(Queryable), + method, + new[] { typeof(TEntity), sort.KeySelector.ReturnType }, + query.Expression, + Expression.Quote(sort.KeySelector)); + + query = query.Provider.CreateQuery(call); + first = false; + } + + return query; + } + + /// + /// Translates a into an , + /// applying, in order: → + /// and + /// → the per-call → eager + /// → + /// → → + /// → + /// → . + /// + /// The type of the entity. + /// The source query (typically the entity set). + /// The query options, or null when no shaping is requested. + /// + /// An optional per-call transformation (e.g. an explicit filter) applied right after the options' + /// own filter and before eager loading, sorting and the remaining shaping. + /// + /// The shaped query. + internal static IQueryable ApplyOptions(this IQueryable source, + QueryOptions options, + Func, IQueryable> internalQueryAction = null) + where TEntity : class + { + var query = source; + + if (options?.BeforeCustomization != null) + { + query = options.BeforeCustomization(query); + } + + if (options?.Specification != null) + { + query = query.Where(options.Specification.SatisfiedBy()); + } + + if (options?.Filter != null) + { + query = query.Where(options.Filter); + } + + if (internalQueryAction != null) + { + query = internalQueryAction(query); + } + + if (options == null) + { + return query; + } + + if (options.IncludePaths.Count > 0) + { + query = query.IncludeMany(options.IncludePaths.ToArray()); + } + + if (options.Sortings.Count > 0) + { + query = query.ApplySorts(options.Sortings); + } + + if (options.AsNoTracking) + { + query = query.AsNoTracking(); + } + + if (options.IgnoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (!string.IsNullOrEmpty(options.Tag)) + { + query = query.TagWith(options.Tag); + } + + if (options.AfterCustomization != null) + { + query = options.AfterCustomization(query); + } + + return query; + } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs index 27daf7b..878172b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Extensions/ServiceCollectionExtensions.cs @@ -1,8 +1,9 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; +using System.Reflection; using eQuantic.Core.Data.EntityFramework.Repository.Options; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Sql; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -31,7 +32,7 @@ public static IServiceCollection AddQueryableRepositories(services, lifetime); AddGenericRepositories(services, lifetime); @@ -44,7 +45,7 @@ public static IServiceCollection AddQueryableRepositories( { var repoOptions = GetOptions(options); var lifetime = repoOptions.GetLifetime(); - + AddUnitOfWork(services, lifetime); AddGenericRepositories(services, lifetime); @@ -57,7 +58,7 @@ public static IServiceCollection AddCustomRepositories(thi { var repoOptions = GetOptions(options); var lifetime = repoOptions.GetLifetime(); - + AddUnitOfWork(services, lifetime); AddRepositories(services, repoOptions); @@ -70,33 +71,33 @@ private static void AddUnitOfWork(IServic { services.TryAdd(new ServiceDescriptor(typeof(TUnitOfWorkInterface), typeof(TUnitOfWorkImpl), lifetime)); services.TryAdd(new ServiceDescriptor(typeof(IQueryableUnitOfWork), sp => sp.GetRequiredService(), lifetime)); - - services.TryAdd(new ServiceDescriptor(typeof(ISqlUnitOfWork), sp => sp.GetRequiredService(), lifetime)); - services.TryAdd(new ServiceDescriptor(typeof(IQueryableUnitOfWork), sp => sp.GetRequiredService(), lifetime)); } private static void AddGenericRepositories(IServiceCollection services, ServiceLifetime lifetime) { - services.TryAdd(new ServiceDescriptor(typeof(IQueryableRepository<,,>), typeof(QueryableRepository<,,>), lifetime)); - services.TryAdd(new ServiceDescriptor(typeof(IAsyncQueryableRepository<,,>), typeof(AsyncQueryableRepository<,,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IRepository<,>), typeof(QueryableRepository<,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IQueryableRepository<,>), typeof(QueryableRepository<,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAsyncRepository<,>), typeof(AsyncQueryableRepository<,>), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(IAsyncQueryableRepository<,>), typeof(AsyncQueryableRepository<,>), lifetime)); } - + private static void AddRepositories(IServiceCollection services, RepositoryOptions repoOptions) { + var lifetime = repoOptions.GetLifetime(); var types = repoOptions.GetAssemblies() - .SelectMany(o => o.GetTypes()) + .SelectMany(GetLoadableTypes) .Where(o => o is { IsAbstract: false, IsInterface: false } && o.GetInterfaces().Any(i => i == typeof(IRepository))); foreach (var type in types) { - AddRepository(typeof(IRepository<,,>), type, services); - AddRepository(typeof(IAsyncRepository<,,>), type, services); - AddRepository(typeof(IQueryableRepository<,,>), type, services); - AddRepository(typeof(IAsyncQueryableRepository<,,>), type, services); + AddRepository(typeof(IRepository<,>), type, services, lifetime); + AddRepository(typeof(IAsyncRepository<,>), type, services, lifetime); + AddRepository(typeof(IQueryableRepository<,>), type, services, lifetime); + AddRepository(typeof(IAsyncQueryableRepository<,>), type, services, lifetime); } } - private static void AddRepository(Type interfaceType, Type type, IServiceCollection services) + private static void AddRepository(Type interfaceType, Type type, IServiceCollection services, ServiceLifetime lifetime) { var interfaces = type.GetInterfaces(); var repoInterface = interfaces.FirstOrDefault(o => @@ -107,11 +108,27 @@ private static void AddRepository(Type interfaceType, Type type, IServiceCollect return; } - var uowType = repoInterface.GenericTypeArguments[0]; - var entityType = repoInterface.GenericTypeArguments[1]; - var keyType = repoInterface.GenericTypeArguments[2]; + var entityType = repoInterface.GenericTypeArguments[0]; + var keyType = repoInterface.GenericTypeArguments[1]; - services.AddTransient(interfaceType.MakeGenericType(uowType, entityType, keyType), type); + // Honour the configured lifetime instead of forcing Transient, and use TryAdd so calling the + // registration twice does not produce duplicate descriptors. + services.TryAdd(new ServiceDescriptor( + interfaceType.MakeGenericType(entityType, keyType), type, lifetime)); + } + + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + // Assembly.GetTypes() throws ReflectionTypeLoadException when a dependency cannot be loaded; + // fall back to the types that did load instead of failing the whole registration at startup. + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(t => t != null)!; + } } private static RepositoryOptions GetOptions(Action options) diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs index d25c758..7345467 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/QueryableRepository.cs @@ -1,379 +1,90 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using eQuantic.Core.Data.EntityFramework.Repository.Read; using eQuantic.Core.Data.EntityFramework.Repository.Write; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; -using eQuantic.Core.Data.Repository.Read; using eQuantic.Core.Data.Repository.Write; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.Repository; [ExcludeFromCodeCoverage] -public class QueryableRepository : - IRepository, TEntity, TKey>, - IQueryableRepository - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class QueryableRepository : + QueryableReadRepository, + IQueryableRepository, + IRepository + where TEntity : class, IEntity { - private readonly IQueryableReadRepository _readRepository; - private readonly IWriteRepository _writeRepository; - private bool _disposed; + private readonly IWriteRepository _writeRepository; /// /// Create a new instance of repository /// /// Associated Unit Of Work - public QueryableRepository(TUnitOfWork unitOfWork) + public QueryableRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { - this.UnitOfWork = unitOfWork; - var readRepository = new QueryableReadRepository(unitOfWork); - readRepository.OwnUnitOfWork = false; - this._readRepository = readRepository; - - var writeRepository = new WriteRepository(unitOfWork); - writeRepository.OwnUnitOfWork = false; - this._writeRepository = writeRepository; + _writeRepository = new WriteRepository(unitOfWork); } - public TUnitOfWork UnitOfWork { get; private set; } - public void Add(TEntity item) { - this._writeRepository.Add(item); - } - - public IEnumerable AllMatching(ISpecification specification, - Action> configuration = default) - { - return this._readRepository.AllMatching(specification, configuration); - } - - public long Count() - { - return this._readRepository.Count(); - } - - public long Count(ISpecification specification) - { - return this._readRepository.Count(specification); - } - - public long Count(Expression> filter) - { - return this._readRepository.Count(filter); - } - - public int Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public int Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public int Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public int? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public int? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public int? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public long Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public long Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public long Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public long? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public long? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public long? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public double Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public double Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public double Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public double? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public double? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public double? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public float Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public float Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public float Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); + _writeRepository.Add(item); } - public float? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public float? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public float? Sum(Expression> filter, Expression> source) + public void AddRange(IEnumerable items) { - return _readRepository.Sum(filter, source); - } - - public decimal Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public decimal Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public decimal Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public decimal? Sum(Expression> source) - { - return _readRepository.Sum(source); - } - public decimal? Sum(ISpecification specification, Expression> source) - { - return _readRepository.Sum(specification, source); - } - public decimal? Sum(Expression> filter, Expression> source) - { - return _readRepository.Sum(filter, source); - } - - public bool All(ISpecification specification, Action> configuration = default) - { - return this._readRepository.All(specification, configuration); - } - - public bool All(Expression> filter, Action> configuration = default) - { - return this._readRepository.All(filter, configuration); - } - - public bool Any(Action> configuration = default) - { - return this._readRepository.Any(configuration); - } - - public bool Any(ISpecification specification, Action> configuration = default) - { - return this._readRepository.Any(specification, configuration); - } - - public bool Any(Expression> filter, Action> configuration = default) - { - return this._readRepository.Any(filter, configuration); + _writeRepository.AddRange(items); } public long DeleteMany(Expression> filter) { - return this._writeRepository.DeleteMany(filter); + return _writeRepository.DeleteMany(filter); } public long DeleteMany(ISpecification specification) { - return this._writeRepository.DeleteMany(specification); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public TEntity Get(TKey id, Action> configuration = default) - { - return this._readRepository.Get(id, configuration); - } - - public IEnumerable GetAll(Action> configuration = default) - { - return this._readRepository.GetAll(configuration); - } - - public IEnumerable GetMapped(Expression> filter, - Expression> map, Action> configuration = default) - { - return this._readRepository.GetMapped(filter, map, configuration); - } - - public IEnumerable GetMapped(ISpecification specification, - Expression> map, Action> configuration = default) - { - return this._readRepository.GetMapped(specification, map, configuration); - } - - public IEnumerable GetFiltered(Expression> filter, - Action> configuration = default) - { - return this._readRepository.GetFiltered(filter, configuration); - } - - public TEntity GetFirst(Expression> filter, Action> configuration = default) - { - return this._readRepository.GetFirst(filter, configuration); - } - - public TEntity GetFirst(ISpecification specification, Action> configuration = default) - { - return this._readRepository.GetFirst(specification, configuration); - } - - public TResult GetFirstMapped(Expression> filter, Expression> map, Action> configuration = default) - { - return this._readRepository.GetFirstMapped(filter, map, configuration); - } - - public TResult GetFirstMapped(ISpecification specification, Expression> map, Action> configuration = default) - { - return this._readRepository.GetFirstMapped(specification, map, configuration); - } - - public IEnumerable GetPaged(int limit, Action> configuration = default) - { - return this._readRepository.GetPaged(limit, configuration); - } - - public IEnumerable GetPaged(ISpecification specification, int limit, - Action> configuration = default) - { - return this._readRepository.GetPaged(specification, limit, configuration); - } - - public IEnumerable GetPaged(Expression> filter, int limit, - Action> configuration = default) - { - return this._readRepository.GetPaged(filter, limit, configuration); - } - - public IEnumerable GetPaged(int pageIndex, int pageSize, Action> configuration = default) - { - return this._readRepository.GetPaged(pageIndex, pageSize, configuration); - } - - public IEnumerable GetPaged(ISpecification specification, int pageIndex, int pageSize, - Action> configuration = default) - { - return this._readRepository.GetPaged(specification, pageIndex, pageSize, configuration); - } - - public IEnumerable GetPaged(Expression> filter, int pageIndex, int pageSize, - Action> configuration = default) - { - return this._readRepository.GetPaged(filter, pageIndex, pageSize, configuration); - } - - public TEntity GetSingle(Expression> filter, Action> configuration = default) - { - return this._readRepository.GetSingle(filter, configuration); - } - - public TEntity GetSingle(ISpecification specification, Action> configuration = default) - { - return this._readRepository.GetSingle(specification, configuration); + return _writeRepository.DeleteMany(specification); } public void Merge(TEntity persisted, TEntity current) { - this._writeRepository.Merge(persisted, current); + _writeRepository.Merge(persisted, current); } public void Modify(TEntity item) { - this._writeRepository.Modify(item); + _writeRepository.Modify(item); } public void Remove(TEntity item) { - this._writeRepository.Remove(item); + _writeRepository.Remove(item); } public void TrackItem(TEntity item) { - this._writeRepository.TrackItem(item); + _writeRepository.TrackItem(item); } public long UpdateMany(Expression> filter, Expression> updateFactory) { - return this._writeRepository.UpdateMany(filter, updateFactory); + return _writeRepository.UpdateMany(filter, updateFactory); } public long UpdateMany(ISpecification specification, Expression> updateFactory) { - return this._writeRepository.UpdateMany(specification, updateFactory); + return _writeRepository.UpdateMany(specification, updateFactory); } - protected virtual void Dispose(bool disposing) + protected override void Dispose(bool disposing) { - if (_disposed) - { - return; - } - if (disposing) { - this._readRepository?.Dispose(); - this._writeRepository?.Dispose(); - UnitOfWork?.Dispose(); + _writeRepository?.Dispose(); } - _disposed = true; + base.Dispose(disposing); } } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs index 153ea46..ac965d3 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/AsyncQueryableReadRepository.cs @@ -1,12 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Options; using eQuantic.Core.Data.Repository.Read; using eQuantic.Linq.Specification; using Microsoft.EntityFrameworkCore; @@ -14,803 +15,223 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Read; [ExcludeFromCodeCoverage] -public class AsyncQueryableReadRepository : - QueryableReadRepository, - IAsyncQueryableReadRepository, - IAsyncReadRepository, TEntity, TKey> - where TUnitOfWork : class, IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class AsyncQueryableReadRepository : + QueryableReadRepository, + IAsyncQueryableReadRepository + where TEntity : class, IEntity { + private const string FilterExpressionCannotBeNull = "Filter expression cannot be null"; + private const string SpecificationCannotBeNull = "Specification cannot be null"; + private const string MapCannotBeNull = "Map expression cannot be null"; - public AsyncQueryableReadRepository(TUnitOfWork unitOfWork) : base(unitOfWork) + public AsyncQueryableReadRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { } - public Task> AllMatchingAsync( - ISpecification specification, - Action> configuration = default) - { - return AllMatchingAsync(specification, configuration, CancellationToken.None); - } - - public async Task> AllMatchingAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => - query - .Where(specification.SatisfiedBy())) - .ToListAsync(cancellationToken); - } - - public Task> AllMatchingAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return AllMatchingAsync(specification, (Action>)null, cancellationToken); - } - - public Task CountAsync(CancellationToken cancellationToken = default) - { - return GetSet().LongCountAsync(cancellationToken); - } - - public Task CountAsync( - ISpecification specification, + public async Task GetAsync(TKey id, QueryOptions options = null, CancellationToken cancellationToken = default) { - if (specification == null) + if (id is null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(id)); } - return this.CountAsync(specification.SatisfiedBy(), cancellationToken); - } - - public Task CountAsync( - Expression> filter, - CancellationToken cancellationToken = default) - { - return GetSet().LongCountAsync(filter, cancellationToken); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } - - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); - } + if (options == null) + { + return await GetSet().FindAsync(id, cancellationToken).ConfigureAwait(false); + } - public Task SumAsync(Expression> source) - { - return GetSet().SumAsync(source); - } - public Task SumAsync(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).SumAsync(source); - } - public Task SumAsync(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).SumAsync(source); + var idExpression = GetSet().GetExpression(id); + return await GetSet().GetQueryable(options, query => query.Where(idExpression)) + .SingleOrDefaultAsync(cancellationToken).ConfigureAwait(false); } - public Task AllAsync( - ISpecification specification, - Action> configuration = default) + public async Task> GetAllAsync(QueryOptions options = null, + CancellationToken cancellationToken = default) { - return AllAsync(specification, configuration, CancellationToken.None); + return await GetSet().GetQueryable(options).ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task AllAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public async Task> GetFilteredAsync(Expression> filter, + QueryOptions options = null, CancellationToken cancellationToken = default) { - if (specification == null) + if (filter == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); } - return AllAsync(specification.SatisfiedBy(), configuration, cancellationToken); + return await GetSet().GetQueryable(options, query => query.Where(filter)) + .ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task AllAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return AllAsync(specification, (Action>)null, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - Action> configuration = default) - { - return AllAsync(filter, configuration, CancellationToken.None); - } - - public Task AllAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return GetQueryable(configuration, e => e).AllAsync(filter, cancellationToken); - } - - public Task AllAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return AllAsync(filter, (Action>)null, cancellationToken); - } - - public Task AnyAsync( - Action> configuration = default) - { - return AnyAsync(configuration, CancellationToken.None); - } - - public Task AnyAsync( - Action> configuration, - CancellationToken cancellationToken) - { - return GetQueryable(configuration, e => e).AnyAsync(cancellationToken); - } - - public Task AnyAsync( - CancellationToken cancellationToken) - { - return AnyAsync((Action>)null, cancellationToken); - } - - public Task AnyAsync( - ISpecification specification, - Action> configuration = default) - { - return AnyAsync(specification, configuration, CancellationToken.None); - } - - public Task AnyAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public async Task> AllMatchingAsync(ISpecification specification, + QueryOptions options = null, CancellationToken cancellationToken = default) { if (specification == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); } - return AnyAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task AnyAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return AnyAsync(specification, (Action>)null, cancellationToken); + return await GetSet().GetQueryable(options, query => query.Where(specification.SatisfiedBy())) + .ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task AnyAsync( - Expression> filter, - Action> configuration = default) + public async Task> GetMappedAsync(Expression> map, + QueryOptions options = null, CancellationToken cancellationToken = default) { - return AnyAsync(filter, configuration, CancellationToken.None); - } - - public Task AnyAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return GetQueryable(configuration, query => query.Where(filter)).AnyAsync(cancellationToken); - } - - public Task AnyAsync( - Expression> filter, - CancellationToken cancellationToken = default) - { - return AnyAsync(filter, (Action>)null, cancellationToken); - } + if (map == null) + { + throw new ArgumentNullException(nameof(map), MapCannotBeNull); + } - public Task> GetAllAsync( - Action> configuration = default) - { - return GetAllAsync(configuration, CancellationToken.None); - } - - public async Task> GetAllAsync( - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(_ => true)) - .ToListAsync(cancellationToken); - } - - public Task> GetAllAsync( - CancellationToken cancellationToken = default) - { - return GetAllAsync((Action>)null, cancellationToken); + return await GetSet().GetQueryable(options).Select(map) + .ToListAsync(cancellationToken).ConfigureAwait(false); } - public Task GetAsync( - TKey id, - Action> configuration = default) + public Task GetFirstAsync(QueryOptions options, CancellationToken cancellationToken = default) { - return GetAsync(id, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).FirstOrDefaultAsync(cancellationToken); } - public Task GetAsync( - TKey id, - Action> configuration, - CancellationToken cancellationToken) + public Task GetFirstMappedAsync(Expression> map, + QueryOptions options, CancellationToken cancellationToken = default) { - if (Equals(id, default(TKey))) + if (map == null) { - throw new ArgumentNullException(nameof(id)); + throw new ArgumentNullException(nameof(map), MapCannotBeNull); } - return GetInternalAsync(id, configuration, cancellationToken); - } - - public Task GetAsync( - TKey id, - CancellationToken cancellationToken) - { - return GetAsync(id, (Action>)null, cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return GetMappedAsync(filter, map, configuration, CancellationToken.None); - } - - public async Task> GetMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(filter)) - .Select(map) - .ToListAsync(cancellationToken); - } - - public Task> GetMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return GetMappedAsync(filter, map, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).Select(map).FirstOrDefaultAsync(cancellationToken); } - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) + public Task GetSingleAsync(QueryOptions options, CancellationToken cancellationToken = default) { - return GetMappedAsync(specification, map, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).SingleOrDefaultAsync(cancellationToken); } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - return GetMappedAsync(specification.SatisfiedBy(), map, configuration, cancellationToken); - } - - public Task> GetMappedAsync( - ISpecification specification, - Expression> map, + public async Task> GetPagedAsync(PageRequest page, QueryOptions options = null, CancellationToken cancellationToken = default) { - return GetMappedAsync(specification, map, (Action>)null, cancellationToken); - } - - public Task> GetFilteredAsync( - Expression> filter, - Action> configuration = default) - { - return GetFilteredAsync(filter, configuration, CancellationToken.None); - } - - public async Task> GetFilteredAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(filter)) - .ToListAsync(cancellationToken); - } - - public Task> GetFilteredAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return GetFilteredAsync(filter, (Action>)null, cancellationToken); - } + if (page == null) + { + throw new ArgumentNullException(nameof(page)); + } - public Task GetFirstAsync( - Expression> filter, - Action> configuration = default) - { - return GetFirstAsync(filter, configuration, CancellationToken.None); - } - - public async Task GetFirstAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) - { - return await GetQueryable(configuration, query => query.Where(filter)) - .FirstOrDefaultAsync(cancellationToken); - } - - public Task GetFirstAsync( - Expression> filter, - CancellationToken cancellationToken) - { - return GetFirstAsync(filter, (Action>)null, cancellationToken); - } + var query = GetSet().GetQueryable(options); + var totalCount = await query.LongCountAsync(cancellationToken).ConfigureAwait(false); + var items = await query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) + .ToListAsync(cancellationToken).ConfigureAwait(false); - public Task GetFirstAsync( - ISpecification specification, - Action> configuration = default) - { - return GetFirstAsync(specification, configuration, CancellationToken.None); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - public Task GetFirstAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) + public async Task> GetPagedAsync(PageRequest page, + Expression> map, QueryOptions options = null, + CancellationToken cancellationToken = default) { - if (specification == null) + if (page == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(page)); } - return GetFirstAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task GetFirstAsync( - ISpecification specification, - CancellationToken cancellationToken) - { - return GetFirstAsync(specification, (Action>)null, cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration = default) - { - return GetFirstMappedAsync(filter, map, configuration, CancellationToken.None); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - if (filter == null) + if (map == null) { - throw new ArgumentNullException(nameof(filter), "Filter expression cannot be null"); + throw new ArgumentNullException(nameof(map), MapCannotBeNull); } - - return GetQueryable(configuration, query => query.Where(filter)) + + var query = GetSet().GetQueryable(options); + var totalCount = await query.LongCountAsync(cancellationToken).ConfigureAwait(false); + var items = await query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) .Select(map) - .FirstOrDefaultAsync(cancellationToken); - } - - public Task GetFirstMappedAsync( - Expression> filter, - Expression> map, - CancellationToken cancellationToken) - { - return GetFirstMappedAsync(filter, map, (Action>)null, cancellationToken); - } + .ToListAsync(cancellationToken).ConfigureAwait(false); - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration = default) - { - return GetFirstMappedAsync(specification, map, configuration, CancellationToken.None); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - return GetFirstMappedAsync(specification.SatisfiedBy(), map, configuration, cancellationToken); - } - - public Task GetFirstMappedAsync( - ISpecification specification, - Expression> map, - CancellationToken cancellationToken = default) + public Task CountAsync(QueryOptions options = null, CancellationToken cancellationToken = default) { - return GetFirstMappedAsync(specification, map, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).LongCountAsync(cancellationToken); } - public Task> GetPagedAsync( - int limit, - Action> configuration = default) - { - return GetPagedAsync(limit, configuration, CancellationToken.None); - } - - public Task> GetPagedAsync( - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return GetPagedAsync((Expression>)null, 1, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - int limit, - CancellationToken cancellationToken = default) + public Task AnyAsync(QueryOptions options = null, CancellationToken cancellationToken = default) { - return GetPagedAsync(limit, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).AnyAsync(cancellationToken); } - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration = default) - { - return GetPagedAsync(specification, limit, configuration, CancellationToken.None); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - Action> configuration, - CancellationToken cancellationToken) + public Task AllAsync(Expression> predicate, QueryOptions options = null, + CancellationToken cancellationToken = default) { - if (specification == null) + if (predicate == null) { - throw new ArgumentNullException(nameof(specification)); + throw new ArgumentNullException(nameof(predicate), FilterExpressionCannotBeNull); } - return GetPagedAsync(specification.SatisfiedBy(), 1, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int limit, - CancellationToken cancellationToken) - { - return GetPagedAsync(specification, limit, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).AllAsync(predicate, cancellationToken); } - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration = default) - { - return GetPagedAsync(filter, limit, configuration, CancellationToken.None); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - Action> configuration, - CancellationToken cancellationToken) - { - return GetPagedAsync(filter, 1, limit, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int limit, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetPagedAsync(filter, limit, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration = default) - { - return GetPagedAsync(pageIndex, pageSize, configuration, - CancellationToken.None); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - return GetPagedAsync((Expression>)null, pageIndex, pageSize, configuration, - cancellationToken); - } - - public Task> GetPagedAsync( - int pageIndex, - int pageSize, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetPagedAsync(pageIndex, pageSize, (Action>)null, - cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration = default) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetPagedAsync(specification, pageIndex, pageSize, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - - return GetPagedAsync(specification.SatisfiedBy(), pageIndex, pageSize, configuration, cancellationToken); - } - - public Task> GetPagedAsync( - ISpecification specification, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return GetPagedAsync(specification, pageIndex, pageSize, (Action>)null, cancellationToken); - } - - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration = default) - { - return GetPagedAsync(filter, pageIndex, pageSize, configuration, CancellationToken.None); - } - - public async Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - Action> configuration, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - var query = GetQueryable(configuration, internalQuery => - { - if (filter != null) - { - internalQuery = internalQuery.Where(filter); - } - - return internalQuery; - }); - - - if (pageIndex < 1) pageIndex = 1; - if (pageSize > 0) - { - return await query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken); - } - - return await query.ToListAsync(cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task> GetPagedAsync( - Expression> filter, - int pageIndex, - int pageSize, - CancellationToken cancellationToken) - { - return GetPagedAsync(filter, pageIndex, pageSize, (Action>)null, cancellationToken); - } - - public Task GetSingleAsync( - Expression> filter, - Action> configuration = default) - { - return GetSingleAsync(filter, configuration, CancellationToken.None); - } - - public async Task GetSingleAsync( - Expression> filter, - Action> configuration, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return await GetQueryable(configuration, query => query.Where(filter)) - .SingleOrDefaultAsync(cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - - public Task GetSingleAsync( - Expression> filter, - CancellationToken cancellationToken) + + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSingleAsync(filter, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - public Task GetSingleAsync( - ISpecification specification, - Action> configuration = default) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSingleAsync(specification, configuration, CancellationToken.None); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - - public Task GetSingleAsync( - ISpecification specification, - Action> configuration, - CancellationToken cancellationToken) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification)); - } - return GetSingleAsync(specification.SatisfiedBy(), configuration, cancellationToken); - } - - public Task GetSingleAsync( - ISpecification specification, - CancellationToken cancellationToken) + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSingleAsync(specification, (Action>)null, cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - private async Task GetInternalAsync(TKey id, Action> configuration = default, + public Task SumAsync(Expression> selector, QueryOptions options = null, CancellationToken cancellationToken = default) { - if (configuration == null) - { - return await GetSet().FindAsync(id, cancellationToken); - } - - var idExpression = GetSet().GetExpression(id); - return await GetQueryable(configuration, query => query.Where(idExpression)) - .SingleOrDefaultAsync(cancellationToken); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - - private IQueryable GetQueryable(Action> configuration, - Func, IQueryable> internalQueryAction) + + public Task SumAsync(Expression> selector, QueryOptions options = null, + CancellationToken cancellationToken = default) { - return GetSet().GetQueryable(configuration, internalQueryAction); + return GetSet().GetQueryable(options).SumAsync(selector, cancellationToken); } - } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs index 243def1..884586b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Read/QueryableReadRepository.cs @@ -1,423 +1,229 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Options; using eQuantic.Core.Data.Repository.Read; using eQuantic.Linq.Specification; namespace eQuantic.Core.Data.EntityFramework.Repository.Read; [ExcludeFromCodeCoverage] -public class QueryableReadRepository : - IQueryableReadRepository, - IReadRepository, TEntity, TKey> - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class QueryableReadRepository : + IQueryableReadRepository + where TEntity : class, IEntity { internal SetBase _dbSet; private bool _disposed; - internal bool OwnUnitOfWork { get; set; } = true; - private const string SpecificationCannotBeNull = "Specification cannot be null"; + private const string FilterExpressionCannotBeNull = "Filter expression cannot be null"; + private const string SpecificationCannotBeNull = "Specification cannot be null"; + /// /// Creates a new instance of the read repository /// /// Associated Unit Of Work - public QueryableReadRepository(TUnitOfWork unitOfWork) + public QueryableReadRepository(IQueryableUnitOfWork unitOfWork) { UnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); } /// - /// + /// The associated queryable unit of work. /// - public TUnitOfWork UnitOfWork { get; private set; } + public IQueryableUnitOfWork UnitOfWork { get; private set; } - public IEnumerable AllMatching(ISpecification specification, - Action> configuration = default) + public TEntity Get(TKey id, QueryOptions options = null) { - if (specification == null) + if (id is null) { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); + throw new ArgumentNullException(nameof(id)); } - return GetQueryable(configuration, query => query.Where(specification.SatisfiedBy())); - } - - public long Count() - { - return GetSet().LongCount(); - } - - public long Count(ISpecification specification) - { - if (specification == null) + if (options == null) { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); + return GetSet().Find(id); } - return this.Count(specification.SatisfiedBy()); - } - - public long Count(Expression> filter) - { - return filter == null ? throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull) : GetSet().LongCount(filter); - } - - - public int Sum(Expression> source) - { - return GetSet().Sum(source); - } - public int Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public int Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public int? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public int? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public int? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public long Sum(Expression> source) - { - return GetSet().Sum(source); - } - public long Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public long Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public long? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public long? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public long? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public double Sum(Expression> source) - { - return GetSet().Sum(source); - } - public double Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public double Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); + var idExpression = GetSet().GetExpression(id); + return GetSet().GetQueryable(options, query => query.Where(idExpression)).SingleOrDefault(); } - public double? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public double? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public double? Sum(Expression> filter, Expression> source) + public IEnumerable GetAll(QueryOptions options = null) { - return GetQueryable(null, query => query.Where(filter)).Sum(source); + return GetSet().GetQueryable(options); } - public float Sum(Expression> source) + public IEnumerable GetFiltered(Expression> filter, QueryOptions options = null) { - return GetSet().Sum(source); - } - public float Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public float Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public float? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public float? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public float? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } + if (filter == null) + { + throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + } - public decimal Sum(Expression> source) - { - return GetSet().Sum(source); - } - public decimal Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public decimal Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); + return GetSet().GetQueryable(options, query => query.Where(filter)); } - public decimal? Sum(Expression> source) - { - return GetSet().Sum(source); - } - public decimal? Sum(ISpecification specification, Expression> source) - { - return GetQueryable(null, query => query.Where(specification.SatisfiedBy())).Sum(source); - } - public decimal? Sum(Expression> filter, Expression> source) - { - return GetQueryable(null, query => query.Where(filter)).Sum(source); - } - - public bool All(ISpecification specification, Action> configuration = default) + public IEnumerable AllMatching(ISpecification specification, QueryOptions options = null) { if (specification == null) { throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); } - return this.All(specification.SatisfiedBy()); + return GetSet().GetQueryable(options, query => query.Where(specification.SatisfiedBy())); } - public bool All(Expression> filter, Action> configuration = default) + public IEnumerable GetMapped(Expression> map, QueryOptions options = null) { - if (filter == null) + if (map == null) { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + throw new ArgumentNullException(nameof(map)); } - return GetQueryable(configuration, _ => _).All(filter); + return GetSet().GetQueryable(options).Select(map); } - public bool Any(Action> configuration = default) + public TEntity GetFirst(QueryOptions options) { - return GetQueryable(configuration, _ => _).Any(); + return GetSet().GetQueryable(options).FirstOrDefault(); } - public bool Any(ISpecification specification, Action> configuration = default) + public TResult GetFirstMapped(Expression> map, QueryOptions options) { - if (specification == null) + if (map == null) { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); + throw new ArgumentNullException(nameof(map)); } - return this.Any(specification.SatisfiedBy()); + return GetSet().GetQueryable(options).Select(map).FirstOrDefault(); } - public bool Any(Expression> filter, Action> configuration = default) + public TEntity GetSingle(QueryOptions options) { - if (filter == null) + return GetSet().GetQueryable(options).SingleOrDefault(); + } + + public PagedResult GetPaged(PageRequest page, QueryOptions options = null) + { + if (page == null) { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + throw new ArgumentNullException(nameof(page)); } - return GetQueryable(configuration, query => query.Where(filter)).Any(); - } + var query = GetSet().GetQueryable(options); + var totalCount = query.LongCount(); + var items = query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) + .ToList(); - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - public TEntity Get(TKey id, Action> configuration = default) + public PagedResult GetPaged(PageRequest page, Expression> map, + QueryOptions options = null) { - if (Equals(id, default(TKey))) + if (page == null) { - throw new ArgumentNullException(nameof(id)); + throw new ArgumentNullException(nameof(page)); } - if (configuration == null) + if (map == null) { - return GetSet().Find(id); + throw new ArgumentNullException(nameof(map)); } - var idExpression = GetSet().GetExpression(id); - return GetQueryable(configuration, query => query.Where(idExpression)) - .SingleOrDefault(); - } + var query = GetSet().GetQueryable(options); + var totalCount = query.LongCount(); + var items = query + .OrderByPrimaryKeyIfUnordered(GetSet().DbContext) + .Skip(page.Skip) + .Take(page.Take) + .Select(map) + .ToList(); - public IEnumerable GetAll(Action> configuration = default) - { - return GetQueryable(configuration, query => query); + return new PagedResult(items, totalCount, page.PageIndex, page.PageSize); } - public IEnumerable GetMapped(Expression> filter, - Expression> map, Action> configuration = default) + public long Count(QueryOptions options = null) { - return GetQueryable(configuration, query => query.Where(filter)).Select(map); + return GetSet().GetQueryable(options).LongCount(); } - public IEnumerable GetMapped(ISpecification specification, - Expression> map, Action> configuration = default) + public bool Any(QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return this.GetMapped(specification.SatisfiedBy(), map, configuration); + return GetSet().GetQueryable(options).Any(); } - public IEnumerable GetFiltered(Expression> filter, - Action> configuration = default) + public bool All(Expression> predicate, QueryOptions options = null) { - if (filter == null) + if (predicate == null) { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); + throw new ArgumentNullException(nameof(predicate), FilterExpressionCannotBeNull); } - return GetQueryable(configuration, query => query.Where(filter)); + return GetSet().GetQueryable(options).All(predicate); } - public TEntity GetFirst(Expression> filter, Action> configuration = default) + public int Sum(Expression> selector, QueryOptions options = null) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); - } - - return GetQueryable(configuration, query => query.Where(filter)).FirstOrDefault(); + return GetSet().GetQueryable(options).Sum(selector); } - public TEntity GetFirst(ISpecification specification, Action> configuration = default) + public int? Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification),SpecificationCannotBeNull); - } - - return GetQueryable(configuration, query => query.Where(specification.SatisfiedBy())).FirstOrDefault(); + return GetSet().GetQueryable(options).Sum(selector); } - public TResult GetFirstMapped(Expression> filter, - Expression> map, Action> configuration = default) + public long Sum(Expression> selector, QueryOptions options = null) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); - } - return GetQueryable(configuration, query => query.Where(filter)) - .Select(map) - .FirstOrDefault(); - } - - public TResult GetFirstMapped(ISpecification specification, - Expression> map, Action> configuration = default) - { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return this.GetFirstMapped(specification.SatisfiedBy(), map, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(int limit, Action> configuration = default) + public long? Sum(Expression> selector, QueryOptions options = null) { - return GetPaged((Expression>)null, 1, limit, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(ISpecification specification, int limit, - Action> configuration = default) + public double Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return GetPaged(specification.SatisfiedBy(), 1, limit, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(Expression> filter, int limit, - Action> configuration = default) + public double? Sum(Expression> selector, QueryOptions options = null) { - return GetPaged(filter, 1, limit, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(int pageIndex, int pageSize, Action> configuration = default) + public float Sum(Expression> selector, QueryOptions options = null) { - return GetPaged((Expression>)null, pageIndex, pageSize, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(ISpecification specification, int pageIndex, int pageSize, - Action> configuration = default) + public float? Sum(Expression> selector, QueryOptions options = null) { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return GetPaged(specification.SatisfiedBy(), pageIndex, pageSize, configuration); + return GetSet().GetQueryable(options).Sum(selector); } - public IEnumerable GetPaged(Expression> filter, int pageIndex, int pageSize, - Action> configuration = default) + public decimal Sum(Expression> selector, QueryOptions options = null) { - var query = GetQueryable(configuration, internalQuery => - { - if (filter != null) - { - internalQuery = internalQuery.Where(filter); - } - - return internalQuery; - }); - if (pageIndex < 1) pageIndex = 1; - return pageSize > 0 ? query.Skip((pageIndex - 1) * pageSize).Take(pageSize) : query; + return GetSet().GetQueryable(options).Sum(selector); } - public TEntity GetSingle(Expression> filter, Action> configuration = default) + public decimal? Sum(Expression> selector, QueryOptions options = null) { - if (filter == null) - { - throw new ArgumentNullException(nameof(filter), FilterExpressionCannotBeNull); - } - - return GetQueryable(configuration, query => query.Where(filter)) - .SingleOrDefault(); + return GetSet().GetQueryable(options).Sum(selector); } - public TEntity GetSingle(ISpecification specification, Action> configuration = default) + public void Dispose() { - if (specification == null) - { - throw new ArgumentNullException(nameof(specification), SpecificationCannotBeNull); - } - - return GetQueryable(configuration, query => - query - .Where(specification.SatisfiedBy())) - .SingleOrDefault(); + Dispose(true); + GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) @@ -427,20 +233,12 @@ protected virtual void Dispose(bool disposing) return; } - if (disposing && OwnUnitOfWork) - { - UnitOfWork?.Dispose(); - } - + // The UnitOfWork is injected, not created here, so its creator (the DI container or the caller) + // owns its lifetime. Disposing it here would tear down the shared DbContext out from under the + // other repositories in the same scope. _disposed = true; } - private IQueryable GetQueryable(Action> configuration, - Func, IQueryable> internalQueryAction) - { - return GetSet().GetQueryable(configuration, internalQueryAction); - } - internal virtual SetBase GetSet() { return _dbSet ??= (SetBase)UnitOfWork.CreateSet(); diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs index 7e59982..17696ca 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/SetBase.cs @@ -7,13 +7,13 @@ using System.Threading.Tasks; using eQuantic.Core.Data.EntityFramework.Repository.Extensions; using eQuantic.Core.Data.Repository; -using eQuantic.Core.Data.Repository.Config; +using eQuantic.Core.Data.Repository.Options; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace eQuantic.Core.Data.EntityFramework.Repository; -public abstract class SetBase : Data.Repository.ISet where TEntity : class, IEntity, new() +public abstract class SetBase : Data.Repository.ISet where TEntity : class, IEntity { internal readonly DbContext DbContext; @@ -38,7 +38,7 @@ public virtual void AddRange(params TEntity[] entities) { InternalDbSet.AddRange(entities); } - + public virtual Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken = default) { return InternalDbSet.AddRangeAsync(entities, cancellationToken); @@ -48,7 +48,7 @@ public virtual Task AddRangeAsync(params TEntity[] entities) { return InternalDbSet.AddRangeAsync(entities); } - + public virtual void ApplyCurrentValues(TEntity original, TEntity current) { //if it is not attached, attach original and set current values @@ -84,7 +84,7 @@ public override bool Equals(object obj) { return InternalDbSet.Equals(obj); } - + public virtual IEnumerable Execute() { return InternalDbSet.ToList(); @@ -97,17 +97,17 @@ public virtual TEntity Find(TKey key) public virtual async Task FindAsync(object[] keyValues, CancellationToken cancellationToken) { - return await InternalDbSet.FindAsync(keyValues, cancellationToken); + return await InternalDbSet.FindAsync(keyValues, cancellationToken).ConfigureAwait(false); } public virtual async Task FindAsync(params object[] keyValues) { - return await InternalDbSet.FindAsync(keyValues); + return await InternalDbSet.FindAsync(keyValues).ConfigureAwait(false); } public virtual async Task FindAsync(TKey key, CancellationToken cancellationToken = default) { - return await InternalDbSet.FindAsync(new object[] { key }, cancellationToken); + return await InternalDbSet.FindAsync(new object[] { key }, cancellationToken).ConfigureAwait(false); } public virtual IEnumerator GetEnumerator() @@ -132,7 +132,7 @@ public virtual void Insert(TEntity item) public virtual async Task InsertAsync(TEntity item, CancellationToken cancellationToken = default) { - await InternalDbSet.AddAsync(item, cancellationToken); + await InternalDbSet.AddAsync(item, cancellationToken).ConfigureAwait(false); } public virtual EntityEntry Remove(TEntity entity) @@ -158,7 +158,7 @@ public virtual void SetModified(TEntity item) { return; } - + //this operation also attach item in object state manager entry.State = EntityState.Modified; } @@ -187,21 +187,28 @@ public virtual void UpdateRange(params TEntity[] entities) { InternalDbSet.UpdateRange(entities); } - + internal Expression> GetExpression(TKey id) { return DbContext.GetFindByKeyExpression(id); } - - protected static TConfig GetConfig(Action configuration) - where TConfig : Configuration, new() - { - var config = new TConfig(); - configuration?.Invoke(config); - return config; - } - public abstract IQueryable GetQueryable(Action configuration, - Func, IQueryable> internalQueryAction) - where TConfig : Configuration, new(); -} \ No newline at end of file + /// + /// Shapes a query from this set using the supplied . The + /// translation (before-customization → specification/filter → the per-call + /// → includes → sortings → no-tracking → ignore + /// query-filters → tag → after-customization) lives in + /// so relational and document providers + /// share it. The query starts from the underlying (not the set + /// wrapper) so it is a real EF queryable and the async materialization operators work even when no + /// shaping is applied. + /// + /// The query options, or null for no shaping. + /// An optional per-call transformation (e.g. an explicit filter). + /// The shaped query. + internal IQueryable GetQueryable(QueryOptions options, + Func, IQueryable> internalQueryAction = null) + { + return InternalDbSet.ApplyOptions(options, internalQueryAction); + } +} diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs index be90da4..2c62fd5 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/AsyncWriteRepository.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; @@ -8,23 +9,32 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Write; -public class AsyncWriteRepository : WriteRepository, - IAsyncWriteRepository - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class AsyncWriteRepository : WriteRepository, + IAsyncWriteRepository + where TEntity : class, IEntity { - public AsyncWriteRepository(TUnitOfWork unitOfWork) : base(unitOfWork) + public AsyncWriteRepository(IQueryableUnitOfWork unitOfWork) : base(unitOfWork) { } - public Task AddAsync(TEntity item) + public Task AddAsync(TEntity item, CancellationToken cancellationToken = default) { if (item == null) { throw new ArgumentNullException(nameof(item)); } - return GetSet().InsertAsync(item); + return GetSet().InsertAsync(item, cancellationToken); + } + + public Task AddRangeAsync(IEnumerable items, CancellationToken cancellationToken = default) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + return GetSet().AddRangeAsync(items, cancellationToken); } public Task DeleteManyAsync(Expression> filter, @@ -93,5 +103,4 @@ public Task UpdateManyAsync(ISpecification specification, return this.UpdateManyAsync(specification.SatisfiedBy(), updateFactory, cancellationToken); } - } diff --git a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs index 2aeca70..71f22cb 100644 --- a/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs +++ b/src/eQuantic.Core.Data.EntityFramework/Repository/Write/WriteRepository.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq.Expressions; using eQuantic.Core.Data.Repository; using eQuantic.Core.Data.Repository.Write; @@ -6,20 +7,21 @@ namespace eQuantic.Core.Data.EntityFramework.Repository.Write; -public class WriteRepository : IWriteRepository - where TUnitOfWork : IQueryableUnitOfWork - where TEntity : class, IEntity, new() +public class WriteRepository : IWriteRepository + where TEntity : class, IEntity { internal SetBase _dbSet; private bool _disposed; - internal bool OwnUnitOfWork { get; set; } = true; - public WriteRepository(TUnitOfWork unitOfWork) + public WriteRepository(IQueryableUnitOfWork unitOfWork) { UnitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); } - public TUnitOfWork UnitOfWork { get; private set; } + /// + /// The associated queryable unit of work. + /// + public IQueryableUnitOfWork UnitOfWork { get; private set; } public void Add(TEntity item) { @@ -31,6 +33,16 @@ public void Add(TEntity item) GetSet().Insert(item); } + public void AddRange(IEnumerable items) + { + if (items == null) + { + throw new ArgumentNullException(nameof(items)); + } + + GetSet().AddRange(items); + } + public long DeleteMany(Expression> filter) { if (filter == null) @@ -51,12 +63,6 @@ public long DeleteMany(ISpecification specification) return DeleteMany(specification.SatisfiedBy()); } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - public void Merge(TEntity persisted, TEntity current) { GetSet().ApplyCurrentValues(persisted, current); @@ -121,6 +127,12 @@ public long UpdateMany(ISpecification specification, Expression Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 10.0.2.0 - eQuantic Systems + 10.1.0.0 net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,32 +12,19 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 10.0.2.0 - 10.0.2.0 - Icon.png - latest + 10.1.0.0 + 10.1.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -49,8 +35,6 @@ - - @@ -69,5 +53,8 @@ <_Parameter1>$(AssemblyName).MongoDb + + <_Parameter1>$(AssemblyName).Relational + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj deleted file mode 100644 index 36f3830..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net6.csproj +++ /dev/null @@ -1,73 +0,0 @@ - - - - - Core Data library for Entity Framework - eQuantic.Core.Data.EntityFramework - 6.1.2.0 - eQuantic Systems - net6.0 - eQuantic.Core.Data.EntityFramework - eQuantic.Core.Data.EntityFramework - eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 6.1.2.0 - 6.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).SqlServer - - - <_Parameter1>$(AssemblyName).PostgreSql - - - <_Parameter1>$(AssemblyName).MySql - - - <_Parameter1>$(AssemblyName).MongoDb - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj deleted file mode 100644 index 988c99a..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net7.csproj +++ /dev/null @@ -1,73 +0,0 @@ - - - - - Core Data library for Entity Framework - eQuantic.Core.Data.EntityFramework - 7.1.2.0 - eQuantic Systems - net7.0 - eQuantic.Core.Data.EntityFramework - eQuantic.Core.Data.EntityFramework - eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 7.1.2.0 - 7.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).SqlServer - - - <_Parameter1>$(AssemblyName).PostgreSql - - - <_Parameter1>$(AssemblyName).MySql - - - <_Parameter1>$(AssemblyName).MongoDb - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj index 4ffe261..ac1500b 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net8.csproj @@ -4,8 +4,7 @@ Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 8.1.2.0 - eQuantic Systems + 8.2.0.0 net8.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework @@ -13,32 +12,19 @@ Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 8.1.2.0 - 8.1.2.0 - Icon.png - latest + 8.2.0.0 + 8.2.0.0 - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -49,8 +35,6 @@ - - @@ -69,5 +53,8 @@ <_Parameter1>$(AssemblyName).MongoDb + + <_Parameter1>$(AssemblyName).Relational + \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj deleted file mode 100644 index b6e5029..0000000 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.Net9.csproj +++ /dev/null @@ -1,73 +0,0 @@ - - - - - Core Data library for Entity Framework - eQuantic.Core.Data.EntityFramework - 9.1.2.0 - eQuantic Systems - net9.0 - eQuantic.Core.Data.EntityFramework - eQuantic.Core.Data.EntityFramework - eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework - - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 9.1.2.0 - 9.1.2.0 - Icon.png - latest - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - <_Parameter1>$(AssemblyName).Tests - - - <_Parameter1>$(AssemblyName).SqlServer - - - <_Parameter1>$(AssemblyName).PostgreSql - - - <_Parameter1>$(AssemblyName).MySql - - - <_Parameter1>$(AssemblyName).MongoDb - - - \ No newline at end of file diff --git a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj index f9b7a38..776afc9 100644 --- a/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj +++ b/src/eQuantic.Core.Data.EntityFramework/eQuantic.Core.Data.EntityFramework.csproj @@ -1,95 +1,44 @@ - + Core Data library for Entity Framework eQuantic.Core.Data.EntityFramework - 4.4.2.0 - eQuantic Systems - net6.0;net7.0;net8.0;net9.0;net10.0 + 4.5.0.0 + net8.0;net10.0 eQuantic.Core.Data.EntityFramework eQuantic.Core.Data.EntityFramework eQuantic;Core;Data;Library;Repository;Pattern - Entity ignorant persistance with Repository Pattern for Entity - Framework + Entity ignorant persistance with Repository Pattern for Entity Framework - https://github.com/eQuantic/core-data-entityframework - ../../artifacts/ - false - false - false - True - https://github.com/eQuantic/core-data-entityframework - Git - LICENSE - README.md - Copyright © 2016 - 4.4.2.0 - 4.4.2.0 - Icon.png - latest + 4.5.0.0 + 4.5.0.0 - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -100,14 +49,15 @@ - - <_Parameter1>$(AssemblyName).Tests + + <_Parameter1>$(AssemblyName).Relational + <_Parameter1>$(AssemblyName).SqlServer @@ -121,4 +71,4 @@ <_Parameter1>$(AssemblyName).MongoDb - \ No newline at end of file + diff --git a/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs new file mode 100644 index 0000000..a2a5591 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/UpdateDefinitionBuilderTests.cs @@ -0,0 +1,48 @@ +using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.MongoDb; +using eQuantic.Core.Data.Repository; + +namespace eQuantic.Core.Data.EntityFramework.MongoDb.Tests; + +/// +/// Guards the fix for the silent data-corruption defect: an update expression that reads the entity +/// (e.g. x => x.Count + 1) was evaluated against a default instance and wrote a constant. It must now +/// be rejected, while constant/closure values keep working. +/// +public class UpdateDefinitionBuilderTests +{ + private sealed class Doc : IEntity + { + public int Count { get; set; } + public string Name { get; set; } = string.Empty; + } + + [Test] + public void BuildUpdateDefinition_ConstantValue_BuildsDefinition() + { + Expression> update = _ => new Doc { Name = "fixed" }; + + var definition = UpdateDefinitionBuilder.BuildUpdateDefinition(update); + + Assert.That(definition, Is.Not.Null); + } + + [Test] + public void BuildUpdateDefinition_CapturedValue_BuildsDefinition() + { + var captured = 42; + Expression> update = _ => new Doc { Count = captured }; + + var definition = UpdateDefinitionBuilder.BuildUpdateDefinition(update); + + Assert.That(definition, Is.Not.Null); + } + + [Test] + public void BuildUpdateDefinition_ExpressionReferencingEntity_Throws() + { + Expression> update = d => new Doc { Count = d.Count + 1 }; + + Assert.Throws(() => UpdateDefinitionBuilder.BuildUpdateDefinition(update)); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj new file mode 100644 index 0000000..a4edb3e --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests/eQuantic.Core.Data.EntityFramework.MongoDb.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs index 2f167dc..0143a25 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ExpressionConverterTests.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Relational; using Microsoft.EntityFrameworkCore.Query; namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs new file mode 100644 index 0000000..e18bd7e --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/ReadRepositoryQueryTests.cs @@ -0,0 +1,224 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Repository.Read; +using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; +using eQuantic.Linq.Specification; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; + +/// +/// Integration coverage (EF Core InMemory) for the read-repository query fixes, ported to the v5 +/// -based read surface: +/// A1 — All/Any must honour the caller's query options. +/// A2 — Get must not reject a default-valued key (e.g. 0) as a null argument. +/// +public class ReadRepositoryQueryTests +{ + private sealed class Product : IEntity + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + + public int GetKey() => Id; + + public void SetKey(int key) => Id = key; + } + + private sealed class TestDbContext(DbContextOptions options) : DbContext(options) + { + public DbSet Products => Set(); + } + + private sealed class NameSpecification(string name) : Specification + { + public override Expression> SatisfiedBy() => p => p.Name == name; + } + + private static DefaultUnitOfWork NewUnitOfWork(out TestDbContext context) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"read-repo-{Guid.NewGuid():N}") + .Options; + context = new TestDbContext(options); + return new DefaultUnitOfWork(new ServiceCollection().BuildServiceProvider(), context); + } + + private static QueryableReadRepository NewRepository(out TestDbContext context) + => new(NewUnitOfWork(out context)); + + private static AsyncQueryableReadRepository NewAsyncRepository(out TestDbContext context) + => new(NewUnitOfWork(out context)); + + [Test] + public void All_WithOptions_HonoursConfiguration() + { + var repository = NewRepository(out var context); + context.Products.AddRange( + new Product { Id = 1, Name = "active" }, + new Product { Id = 2, Name = "inactive" }); + context.SaveChanges(); + + // The "inactive" row does not satisfy the predicate, so without the options being applied All() + // would evaluate over both rows and return false. The options narrow the query to the "active" + // row, so the fixed code returns true. + var spec = new NameSpecification("active"); + var result = repository.All( + spec.SatisfiedBy(), + new QueryOptions().WithAfterCustomization(q => q.Where(p => p.Name == "active"))); + + Assert.That(result, Is.True); + } + + [Test] + public void Any_WithOptions_HonoursConfiguration() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 1, Name = "active" }); + context.SaveChanges(); + + // The options filter out every row, so Any() must return false once they are applied. + var result = repository.Any( + new QueryOptions() + .Where(new NameSpecification("active")) + .WithAfterCustomization(q => q.Where(_ => false))); + + Assert.That(result, Is.False); + } + + [Test] + public void Get_WithDefaultValuedKey_DoesNotThrow() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 1, Name = "active" }); + context.SaveChanges(); + + // Key 0 is a valid (if absent) value for a value-typed key; it must not be treated as null. + Product? found = null; + Assert.DoesNotThrow(() => found = repository.Get(0)); + Assert.That(found, Is.Null); + } + + [Test] + public void Get_WithExistingKey_ReturnsEntity() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 5, Name = "found" }); + context.SaveChanges(); + + var found = repository.Get(5); + + Assert.That(found, Is.Not.Null); + Assert.That(found!.Name, Is.EqualTo("found")); + } + + [Test] + public void Get_WithOptions_UsesKeyExpression_ReturnsEntity() + { + var repository = NewRepository(out var context); + context.Products.Add(new Product { Id = 7, Name = "seven" }); + context.SaveChanges(); + + // Non-null options route Get through GetFindByKeyExpression instead of DbSet.Find. + var found = repository.Get(7, new QueryOptions()); + + Assert.That(found, Is.Not.Null); + Assert.That(found!.Name, Is.EqualTo("seven")); + } + + [Test] + public void GetFindByKeyExpression_DoesNotEmbedKeyValueAsLiteral() + { + _ = NewUnitOfWork(out var context); + + var expression = context.GetFindByKeyExpression(5); + + Assert.That(expression, Is.Not.Null); + var literalFinder = new ConstantValueFinder(5); + literalFinder.Visit(expression); + Assert.That(literalFinder.Found, Is.False, + "The key value must be parameterized (held in a closure), not embedded as a literal constant."); + } + + [Test] + public async System.Threading.Tasks.Task GetAllAsync_ReturnsAllEntities() + { + var repository = NewAsyncRepository(out var context); + context.Products.AddRange( + new Product { Id = 1, Name = "a" }, + new Product { Id = 2, Name = "b" }, + new Product { Id = 3, Name = "c" }); + context.SaveChanges(); + + var all = await repository.GetAllAsync(); + + Assert.That(all.Count(), Is.EqualTo(3)); + } + + [Test] + public void GetPaged_WithoutSorting_OrdersByPrimaryKeyDeterministically() + { + var repository = NewRepository(out var context); + // Insert out of key order; without a fallback OrderBy the page order would be undefined. + context.Products.AddRange( + new Product { Id = 3, Name = "c" }, + new Product { Id = 1, Name = "a" }, + new Product { Id = 2, Name = "b" }); + context.SaveChanges(); + + var firstPage = repository.GetPaged(PageRequest.Of(1, 2)).Items; + + Assert.That(firstPage.Select(p => p.Id), Is.EqualTo(new[] { 1, 2 })); + } + + [Test] + public void GetPaged_WithExplicitOrdering_IsPreserved() + { + var repository = NewRepository(out var context); + context.Products.AddRange( + new Product { Id = 1, Name = "a" }, + new Product { Id = 2, Name = "b" }, + new Product { Id = 3, Name = "c" }); + context.SaveChanges(); + + // Caller orders descending; the primary-key fallback must NOT override it. + var firstPage = repository + .GetPaged( + PageRequest.Of(1, 2), + new QueryOptions().WithAfterCustomization(q => q.OrderByDescending(p => p.Id))) + .Items; + + Assert.That(firstPage.Select(p => p.Id), Is.EqualTo(new[] { 3, 2 })); + } + + [Test] + public void OrderByPrimaryKeyIfUnordered_AlreadyOrdered_ReturnsSameQuery() + { + _ = NewUnitOfWork(out var context); + var ordered = context.Set().OrderByDescending(p => p.Name); + + var result = ordered.OrderByPrimaryKeyIfUnordered(context); + + Assert.That(result, Is.SameAs(ordered)); + } + + private sealed class ConstantValueFinder(object target) : ExpressionVisitor + { + public bool Found { get; private set; } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (Equals(node.Value, target)) + { + Found = true; + } + + return base.VisitConstant(node); + } + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs new file mode 100644 index 0000000..91f2ed0 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/SqlExecutorParameterizationTests.cs @@ -0,0 +1,87 @@ +using System; +using eQuantic.Core.Data.EntityFramework.Relational.Repository; +using eQuantic.Core.Data.EntityFramework.Relational.Sql; +using eQuantic.Core.Data.EntityFramework.SqlServer.Repository; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.SqlServer.Tests; + +/// +/// Guards the fix for the SQL-injection defect: parameter values must be emitted as placeholders, +/// never interpolated into the SQL text. The implementation now lives in the shared +/// ; SQL Server supplies the EXEC dialect. +/// +public class SqlExecutorParameterizationTests +{ + private const string Malicious = "'; DROP TABLE Users; --"; + + private sealed class TestDbContext(DbContextOptions options) : DbContext(options); + + private static DefaultUnitOfWork NewSqlServerUnitOfWork() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"sqlexec-{Guid.NewGuid():N}") + .Options; + return new DefaultUnitOfWork(new ServiceCollection().BuildServiceProvider(), new TestDbContext(options)); + } + + [Test] + public void GetQueryFunction_EmitsPositionalPlaceholders_NotValues() + { + var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); + + var sql = RelationalSqlExecutor.GetQueryFunction("dbo.GetUser", config); + + Assert.That(sql, Does.Contain("{0}")); + Assert.That(sql, Does.Contain("{1}")); + Assert.That(sql, Does.Not.Contain("DROP TABLE")); + Assert.That(sql, Does.Not.Contain(Malicious)); + } + + [Test] + public void BuildProcedureSql_SqlServer_UsesExecWithNamedPlaceholders_NotValues() + { + var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); + + var sql = NewSqlServerUnitOfWork().BuildProcedureSql("dbo.DoWork", config); + + Assert.That(sql, Does.StartWith("EXEC dbo.DoWork")); + Assert.That(sql, Does.Contain("@Param0")); + Assert.That(sql, Does.Contain("@Param1")); + Assert.That(sql, Does.Not.Contain("DROP TABLE")); + Assert.That(sql, Does.Not.Contain(Malicious)); + } + + [Test] + public void BuildProcedureSql_UsesProvidedParameterNames() + { + var config = new DefaultSqlConfiguration() + .WithParameters(ParamValue.Create("userId", 7), ParamValue.Create("state", "active")); + + var sql = NewSqlServerUnitOfWork().BuildProcedureSql("dbo.DoWork", config); + + Assert.That(sql, Does.Contain("@userId")); + Assert.That(sql, Does.Contain("@state")); + } + + [Test] + public void GetParameterValues_PreservesValues_ForPositionalBinding() + { + var config = new DefaultSqlConfiguration().WithParameters(Malicious, 42); + + var values = RelationalSqlExecutor.GetParameterValues(config); + + Assert.That(values, Has.Length.EqualTo(2)); + Assert.That(values, Does.Contain(Malicious)); + Assert.That(values, Does.Contain(42)); + } + + [Test] + public void GetQueryFunction_EmptyForNoParameters() + { + var sql = RelationalSqlExecutor.GetQueryFunction("dbo.NoArgs", new DefaultSqlConfiguration()); + + Assert.That(sql, Does.Not.Contain("{0}")); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj index 588e730..e240577 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests/eQuantic.Core.Data.EntityFramework.SqlServer.Tests.csproj @@ -10,6 +10,8 @@ + + @@ -21,7 +23,7 @@ - + diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs new file mode 100644 index 0000000..6a060d9 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeEntity.cs @@ -0,0 +1,18 @@ +using eQuantic.Core.Data.Repository; + +namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +/// +/// Minimal entity used to close the generic type parameters of the repositories under test. +/// Implements via GetKey/SetKey as required by the v5 +/// read-repository contracts. +/// +internal sealed class FakeEntity : IEntity +{ + public int Id { get; set; } + public string? Name { get; set; } + + public int GetKey() => Id; + + public void SetKey(int key) => Id = key; +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs new file mode 100644 index 0000000..7e6723d --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeQueryableUnitOfWork.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using eQuantic.Core.Data.Repository; +using eQuantic.Core.Data.Repository.Options; + +namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +/// +/// A non-relational unit of work: it implements but not the +/// relational SQL unit-of-work contract — mirroring the MongoDb provider. Members throw because the +/// DI-registration tests only inspect the service collection; the fake is never instantiated or +/// resolved. +/// +internal sealed class FakeQueryableUnitOfWork : IQueryableUnitOfWork +{ + public int DisposeCount { get; private set; } + + public void Dispose() => DisposeCount++; + + public int Commit() => throw new NotSupportedException(); + public int Commit(Action options) => throw new NotSupportedException(); + public int CommitAndRefreshChanges() => throw new NotSupportedException(); + public int CommitAndRefreshChanges(Action options) => throw new NotSupportedException(); + + public Task CommitAndRefreshChangesAsync(CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task CommitAndRefreshChangesAsync(Action options, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public Task CommitAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(); + + public Task CommitAsync(Action options, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public void RollbackChanges() => throw new NotSupportedException(); + + public IRepository GetRepository() + where TEntity : class, IEntity + => throw new NotSupportedException(); + + public IAsyncRepository GetAsyncRepository() + where TEntity : class, IEntity + => throw new NotSupportedException(); + + public eQuantic.Core.Data.Repository.ISet CreateSet() where TEntity : class, IEntity + => throw new NotSupportedException(); + + public IQueryableRepository GetQueryableRepository() + where TEntity : class, IEntity + => throw new NotSupportedException(); + + public IAsyncQueryableRepository GetAsyncQueryableRepository() + where TEntity : class, IEntity + => throw new NotSupportedException(); +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs new file mode 100644 index 0000000..2e350b0 --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/Fakes/FakeRepository.cs @@ -0,0 +1,15 @@ +using eQuantic.Core.Data.EntityFramework.Repository; + +namespace eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +/// +/// A concrete repository so the assembly-scanning registration (AddCustomRepositories) has +/// something to discover. It inherits the marker IRepository transitively through +/// . +/// +internal sealed class FakeRepository : QueryableRepository +{ + public FakeRepository(FakeQueryableUnitOfWork unitOfWork) : base(unitOfWork) + { + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs new file mode 100644 index 0000000..82365df --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/RepositoryDisposalTests.cs @@ -0,0 +1,48 @@ +using eQuantic.Core.Data.EntityFramework.Repository; +using eQuantic.Core.Data.EntityFramework.Tests.Fakes; + +namespace eQuantic.Core.Data.EntityFramework.Tests; + +/// +/// Covers unit-of-work ownership on disposal. The repository receives its UnitOfWork by +/// injection, so it must NOT dispose it (its creator — the DI container or the caller — owns the +/// lifetime). This also removes the previous double-dispose of the shared DbContext. +/// +public class RepositoryDisposalTests +{ + [Test] + public void Dispose_AsyncQueryableRepository_DoesNotDisposeInjectedUnitOfWork() + { + var unitOfWork = new FakeQueryableUnitOfWork(); + var repository = new AsyncQueryableRepository(unitOfWork); + + repository.Dispose(); + + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(0), + "The repository must not dispose a UnitOfWork it did not create."); + } + + [Test] + public void Dispose_AsyncQueryableRepository_IsIdempotent() + { + var unitOfWork = new FakeQueryableUnitOfWork(); + var repository = new AsyncQueryableRepository(unitOfWork); + + repository.Dispose(); + repository.Dispose(); + + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(0)); + } + + [Test] + public void Dispose_QueryableReadRepository_DoesNotDisposeInjectedUnitOfWork() + { + var unitOfWork = new FakeQueryableUnitOfWork(); + var repository = new eQuantic.Core.Data.EntityFramework.Repository.Read + .QueryableReadRepository(unitOfWork); + + repository.Dispose(); + + Assert.That(unitOfWork.DisposeCount, Is.EqualTo(0)); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..75a129e --- /dev/null +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,98 @@ +using System.Linq; +using eQuantic.Core.Data.EntityFramework.Repository.Extensions; +using eQuantic.Core.Data.EntityFramework.Tests.Fakes; +using eQuantic.Core.Data.Repository; +using Microsoft.Extensions.DependencyInjection; + +namespace eQuantic.Core.Data.EntityFramework.Tests; + +/// +/// Guards the DI-registration behaviour of the base (SQL-agnostic) registration: the generic +/// repositories are wired, is registered exactly once, and no +/// relational SQL unit-of-work contract is registered (that moved to the Relational provider in v5). +/// +public class ServiceCollectionExtensionsTests +{ + [Test] + public void AddQueryableRepositories_WiresGenericRepositories() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + Assert.That(services.Any(d => d.ServiceType == typeof(IQueryableRepository<,>)), Is.True, + "The open-generic IQueryableRepository must be registered by the base registration."); + Assert.That(services.Any(d => d.ServiceType == typeof(IAsyncQueryableRepository<,>)), Is.True, + "The open-generic IAsyncQueryableRepository must be registered by the base registration."); + } + + [Test] + public void AddQueryableRepositories_WiresPlainRepositories_SoGetRepositoryResolves() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + // IUnitOfWork.GetRepository / GetAsyncRepository return IRepository<,> / IAsyncRepository<,> + // (the plain siblings of the queryable interfaces). The generic registration must also serve + // these — the concrete queryable repositories implement the plain interfaces too — otherwise the + // contract's headline accessors throw at runtime. + Assert.That(services.Any(d => d.ServiceType == typeof(IRepository<,>)), Is.True, + "The open-generic IRepository must be registered so GetRepository resolves."); + Assert.That(services.Any(d => d.ServiceType == typeof(IAsyncRepository<,>)), Is.True, + "The open-generic IAsyncRepository must be registered so GetAsyncRepository resolves."); + } + + [Test] + public void AddQueryableRepositories_IsSqlAgnostic_DoesNotRegisterSqlUnitOfWork() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + // ISqlUnitOfWork moved out of eQuantic.Core.Data into the Relational provider in v5, so the base + // registration is SQL-agnostic and must not register it. Checked by name so this base test does + // not reference the moved type. + Assert.That(services.Any(d => d.ServiceType.Name == "ISqlUnitOfWork"), Is.False, + "The base registration must not register ISqlUnitOfWork (it moved to the Relational provider)."); + } + + [Test] + public void AddQueryableRepositories_RegistersQueryableUnitOfWork_ExactlyOnce() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + Assert.That(services.Count(d => d.ServiceType == typeof(IQueryableUnitOfWork)), Is.EqualTo(1), + "IQueryableUnitOfWork must be registered exactly once (the duplicate TryAdd was removed)."); + } + + [Test] + public void AddQueryableRepositories_RegistersConcreteUnitOfWork_AsQueryableUnitOfWork() + { + var services = new ServiceCollection(); + + services.AddQueryableRepositories(); + + Assert.That( + services.Any(d => d.ServiceType == typeof(IQueryableUnitOfWork) + && d.ImplementationType == typeof(FakeQueryableUnitOfWork)), + Is.True); + } + + [Test] + public void AddCustomRepositories_HonoursConfiguredLifetime() + { + var services = new ServiceCollection(); + + services.AddCustomRepositories(o => o + .AddLifetime(ServiceLifetime.Scoped) + .FromAssembly(typeof(FakeRepository).Assembly)); + + var descriptor = services.FirstOrDefault(d => d.ImplementationType == typeof(FakeRepository)); + Assert.That(descriptor, Is.Not.Null, "The scanned repository should have been registered."); + Assert.That(descriptor!.Lifetime, Is.EqualTo(ServiceLifetime.Scoped), + "AddRepository must honour the configured lifetime instead of forcing Transient."); + } +} diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs b/tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs deleted file mode 100644 index 5fcc4bb..0000000 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/UnitTest1.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace eQuantic.Core.Data.EntityFramework.Tests; - -public class Tests -{ - [SetUp] - public void Setup() - { - } - - [Test] - public void Test1() - { - Assert.Pass(); - } -} \ No newline at end of file diff --git a/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj b/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj index a8a9423..ebfc570 100644 --- a/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj +++ b/tests/eQuantic.Core.Data.EntityFramework.Tests/eQuantic.Core.Data.EntityFramework.Tests.csproj @@ -10,6 +10,8 @@ + +