├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── Directory.Build.props ├── Dommel.sln ├── LICENSE ├── NuGet.Config ├── README.md ├── assets ├── dapper-plus-sponsor.png └── entity-framework-extensions-sponsor.png ├── build.ps1 ├── src ├── Dommel.Json │ ├── AssemblyInfo.cs │ ├── Dommel.Json.csproj │ ├── DommelJsonMapper.cs │ ├── DommelJsonOptions.cs │ ├── IJsonSqlBuilder.cs │ ├── JsonDataAttribute.cs │ ├── JsonObjectTypeHandler.cs │ ├── JsonPropertyResolver.cs │ └── JsonSqlExpression.cs └── Dommel │ ├── Any.cs │ ├── AssemblyInfo.cs │ ├── AutoMultiMap.cs │ ├── Cache.cs │ ├── ColumnPropertyInfo.cs │ ├── Count.cs │ ├── DateOnlyTypeHandler.cs │ ├── DefaultColumnNameResolver.cs │ ├── DefaultForeignKeyPropertyResolver.cs │ ├── DefaultKeyPropertyResolver.cs │ ├── DefaultPropertyResolver.cs │ ├── DefaultTableNameResolver.cs │ ├── Delete.cs │ ├── Dommel.csproj │ ├── DommelMapper.cs │ ├── ForeignKeyRelation.cs │ ├── From.cs │ ├── Get.cs │ ├── IColumnNameResolver.cs │ ├── IForeignKeyPropertyResolver.cs │ ├── IKeyPropertyResolver.cs │ ├── IPropertyResolver.cs │ ├── ISqlBuilder.cs │ ├── ITableNameResolver.cs │ ├── IgnoreAttribute.cs │ ├── Insert.cs │ ├── MultiMap.cs │ ├── MySqlSqlBuilder.cs │ ├── PostgresSqlBuilder.cs │ ├── Project.cs │ ├── Resolvers.cs │ ├── Select.cs │ ├── SelectAutoMultiMap.cs │ ├── SelectAutoMultiMapAsync.cs │ ├── SelectMultiMap.cs │ ├── SelectMultiMapAsync.cs │ ├── SqlExpression.cs │ ├── SqlServerCeSqlBuilder.cs │ ├── SqlServerSqlBuilder.cs │ ├── SqliteSqlBuilder.cs │ └── Update.cs └── test ├── Dommel.IntegrationTests ├── AnyTests.cs ├── AutoMultiMapTests.cs ├── CountTests.cs ├── Databases │ ├── DatabaseDriver.cs │ ├── MySqlDatabaseDriver.cs │ ├── PostgresDatabaseDriver.cs │ └── SqlServerDatabaseDriver.cs ├── DeleteTests.cs ├── Dommel.IntegrationTests.csproj ├── FirstOrDefaultTests.cs ├── FromTests.cs ├── GetAllTests.cs ├── GetTests.cs ├── Infrastructure │ ├── CI.cs │ ├── DatabaseCollection.cs │ ├── DatabaseFixture.cs │ └── DatabaseTestData.cs ├── InsertNonGeneratedColumnTests.cs ├── InsertOutputParameterTests.cs ├── InsertTests.cs ├── Models.cs ├── MultiMapTests.cs ├── PagingTests.cs ├── ProjectTests.cs ├── ResolversTests.cs ├── SelectAutoMultiMapTests.cs ├── SelectMultiMapTests.cs ├── SelectTests.cs ├── TestSample.cs ├── UpdateTests.cs └── coverage.cmd ├── Dommel.Json.IntegrationTests ├── Databases │ ├── JsonMySqlDatabaseDriver.cs │ ├── JsonPostgresDatabaseDriver.cs │ └── JsonSqlServerDatabaseDriver.cs ├── DeleteTests.cs ├── Dommel.Json.IntegrationTests.csproj ├── Infrastructure │ ├── JsonDatabaseCollection.cs │ ├── JsonDatabaseFixture.cs │ └── JsonDatabaseTestData.cs ├── InsertTests.cs ├── Models.cs ├── SelectTests.cs └── UpdateTests.cs ├── Dommel.Json.Tests ├── Dommel.Json.Tests.csproj ├── DommelJsonMapperTests.cs ├── DommelJsonOptionsTests.cs ├── JsonObjectTypeHandlerTests.cs ├── JsonPropertyResolverTests.cs ├── JsonSqlExpressionTests.cs ├── LikeTests.cs └── Models.cs └── Dommel.Tests ├── AnyTests.cs ├── AutoMultiMapTests.cs ├── CacheTests.cs ├── ColumnPropertyInfoTests.cs ├── CountTests.cs ├── DefaultColumnNameResolverTests.cs ├── DefaultForeignKeyPropertyResolverTests.cs ├── DefaultKeyPropertyResolverTests.cs ├── DefaultPropertyResolverTests.cs ├── DefaultTableNameResolverTests.cs ├── Dommel.Tests.csproj ├── DummySqlBuilder.cs ├── Models.cs ├── MultiMapTests.cs ├── MySqlSqlBuilderTests.cs ├── ParameterPrefixTest.cs ├── PostgresSqlBuilderTests.cs ├── ProjectTests.cs ├── ResolversTests.cs ├── SqlExpressions ├── BooleanExpressionTests.cs ├── DynamicExpressionTests.cs ├── LikeTests.cs ├── NullExpressionTests.cs ├── PageTests.cs ├── SelectExpressionTests.cs ├── SqlExpressionTests.cs └── WhereExpressionTests.cs ├── SqlServerCeSqlBuilderTests.cs ├── SqlServerSqlBuilderTests.cs ├── SqliteSqlBuilderTests.cs ├── TypeMapProviderTests.cs └── coverage.cmd /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: henkmollema 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | .nuget/ 4 | packages/ 5 | artifacts/ 6 | pack/ 7 | PublishProfiles/ 8 | .vs/ 9 | debugSettings.json 10 | project.lock.json 11 | *.user 12 | *.suo 13 | nuget.exe 14 | *.userprefs 15 | *DS_Store 16 | *.*sdf 17 | *.ipch 18 | .settings 19 | *.sln.ide 20 | .build/ 21 | *.ldf 22 | coveragereport/ 23 | coverage*.opencover.xml 24 | coverage*.json 25 | .idea/ 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 6.0 4 | dist: xenial 5 | env: 6 | global: 7 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 8 | - DOTNET_CLI_TELEMETRY_OPTOUT: true 9 | - TRAVIS: true 10 | services: 11 | - mysql 12 | - postgresql 13 | branches: 14 | only: 15 | - master 16 | script: 17 | - dotnet restore 18 | - dotnet build 19 | - dotnet test test/Dommel.Tests --no-build 20 | - dotnet test test/Dommel.IntegrationTests --no-build 21 | - dotnet test test/Dommel.Json.Tests --no-build 22 | - dotnet test test/Dommel.Json.IntegrationTests --no-build 23 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright © Henk Mollema 2014 4 | 3.4.0 5 | Henk Mollema 6 | latest 7 | enable 8 | https://github.com/henkmollema/Dommel 9 | MIT 10 | git 11 | https://github.com/henkmollema/Dommel 12 | true 13 | true 14 | embedded 15 | README.md 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Henk Mollema 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dommel 2 | CRUD operations with Dapper made simple. 3 | 4 | | Build | NuGet | MyGet | Test Coverage | 5 | | ----- | ----- | ----- | ------------- | 6 | | [![Travis](https://img.shields.io/travis/com/henkmollema/Dommel?style=flat-square)](https://app.travis-ci.com/github/henkmollema/Dommel) | [![NuGet](https://img.shields.io/nuget/vpre/Dommel.svg?style=flat-square)](https://www.nuget.org/packages/Dommel) | [![MyGet Pre Release](https://img.shields.io/myget/dommel-ci/vpre/Dommel.svg?style=flat-square)](https://www.myget.org/feed/dommel-ci/package/nuget/Dommel) | [![codecov](https://codecov.io/gh/henkmollema/Dommel/branch/master/graph/badge.svg)](https://codecov.io/gh/henkmollema/Dommel) | 7 | 8 |
9 | 10 | Dommel provides a convenient API for CRUD operations using extension methods on the `IDbConnection` interface. The SQL queries are generated based on your POCO entities. Dommel also supports LINQ expressions which are being translated to SQL expressions. [Dapper](https://github.com/StackExchange/Dapper) is used for query execution and object mapping. 11 | 12 | There are several extensibility points available to change the behavior of resolving table names, column names, the key property and POCO properties. See [Extensibility](https://www.learndapper.com/extensions/dommel#extensibility) for more details. 13 | 14 | ## Installing Dommel 15 | 16 | Dommel is available on [NuGet](https://www.nuget.org/packages/Dommel). 17 | 18 | ### Install using the .NET CLI: 19 | ``` 20 | dotnet add package Dommel 21 | ``` 22 | 23 | ### Install using the NuGet Package Manager: 24 | ``` 25 | Install-Package Dommel 26 | ``` 27 | 28 | ## Documentation 29 | 30 | The documentation is available at **[Learn Dapper](https://www.learndapper.com/extensions/dommel)**. 31 | 32 | ## Sponsors 33 | [Dapper Plus](https://dapper-plus.net/) and [Entity Framework Extensions](https://entityframework-extensions.net/) are major sponsors and are proud to contribute to the development of Dommel. 34 | 35 | [![Dapper Plus](https://raw.githubusercontent.com/henkmollema/Dommel/refs/heads/master/assets/dapper-plus-sponsor.png)](https://dapper-plus.net/bulk-insert) 36 | 37 | [![Entity Framework Extensions](https://raw.githubusercontent.com/henkmollema/Dommel/refs/heads/master/assets/entity-framework-extensions-sponsor.png)](https://entityframework-extensions.net/bulk-insert) 38 | -------------------------------------------------------------------------------- /assets/dapper-plus-sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkmollema/Dommel/ece095d3dcbd67a02c6f31b26861434da35bc669/assets/dapper-plus-sponsor.png -------------------------------------------------------------------------------- /assets/entity-framework-extensions-sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henkmollema/Dommel/ece095d3dcbd67a02c6f31b26861434da35bc669/assets/entity-framework-extensions-sponsor.png -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | function Exec 2 | { 3 | [CmdletBinding()] 4 | param( 5 | [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, 6 | [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) 7 | ) 8 | & $cmd 9 | if ($lastexitcode -ne 0) { 10 | throw ("Exec: " + $errorMessage) 11 | } 12 | } 13 | 14 | if(Test-Path .\artifacts) { Remove-Item .\artifacts -Force -Recurse } 15 | 16 | exec { & dotnet restore } 17 | 18 | # 19 | # Determine version numbers 20 | $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; 21 | $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; 22 | $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] 23 | $commitHash = $(git rev-parse --short HEAD) 24 | $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] 25 | echo "build: Build version suffix is $buildSuffix" 26 | 27 | exec { & dotnet build Dommel.sln -c Release --version-suffix=$buildSuffix /p:CI=true } 28 | 29 | # 30 | # Execute tests 31 | echo "build: Executing tests" 32 | exec { & dotnet test test/Dommel.Tests -c Release --no-build } 33 | exec { & dotnet test test/Dommel.IntegrationTests -c Release --no-build } 34 | exec { & dotnet test test/Dommel.Json.Tests -c Release --no-build } 35 | exec { & dotnet test test/Dommel.Json.IntegrationTests -c Release --no-build } 36 | 37 | echo "build: Calculating code coverage metrics" 38 | 39 | # 40 | # Test coverage for Dommel 41 | 42 | # Create the first coverage in the coverlet JSON format to allow merging 43 | exec { & dotnet test test/Dommel.Tests -c Release --no-build /p:CollectCoverage=true } 44 | 45 | # Merge this coverage output with the previous coverage output, this time 46 | # create a report using the opencover format which codecov can parse 47 | Push-Location -Path "test/Dommel.IntegrationTests" 48 | exec { & dotnet test -c Release --no-build /p:CollectCoverage=true /p:MergeWith="..\Dommel.Tests\coverage.json" /p:CoverletOutputFormat=opencover } 49 | if ($env:APPVEYOR_BUILD_NUMBER) { 50 | exec { & codecov -f "coverage.opencover.xml" } 51 | } 52 | Pop-Location 53 | 54 | # 55 | # Test coverage for Dommel.Json 56 | exec { & dotnet test test/Dommel.Json.Tests -c Release --no-build /p:CollectCoverage=true /p:Include="[Dommel.Json]*" } 57 | 58 | Push-Location -Path "test/Dommel.Json.IntegrationTests" 59 | exec { & dotnet test -c Release --no-build /p:CollectCoverage=true /p:Include="[Dommel.Json]*" /p:MergeWith="..\Dommel.Json.Tests\coverage.json" /p:CoverletOutputFormat=opencover } 60 | if ($env:APPVEYOR_BUILD_NUMBER) { 61 | exec { & codecov -f "coverage.opencover.xml" } 62 | } 63 | Pop-Location 64 | 65 | # 66 | # Create artifacts 67 | if ($env:APPVEYOR_BUILD_NUMBER) { 68 | $versionSuffix = "beta.{0}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10) 69 | } 70 | else { 71 | $versionSuffix = $suffix 72 | } 73 | 74 | echo "build: Creating NuGet package with suffix $versionSuffix" 75 | exec { & dotnet pack .\src\Dommel\Dommel.csproj -c Release -o .\artifacts --no-build --version-suffix=$versionSuffix } 76 | exec { & dotnet pack .\src\Dommel.Json\Dommel.Json.csproj -c Release -o .\artifacts --no-build --version-suffix=$versionSuffix } 77 | -------------------------------------------------------------------------------- /src/Dommel.Json/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("Dommel.Json.Tests")] 3 | [assembly: InternalsVisibleTo("Dommel.Json.IntegrationTests")] -------------------------------------------------------------------------------- /src/Dommel.Json/Dommel.Json.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net8.0;net9.0 4 | JSON support for Dommel. 5 | dommel;dapper;json 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Dommel.Json/DommelJsonMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using Dapper; 5 | 6 | namespace Dommel.Json; 7 | 8 | /// 9 | /// Extensions to configure JSON support for Dommel. 10 | /// 11 | public static class DommelJsonMapper 12 | { 13 | /// 14 | /// Configures Dommel JSON support on the entities in the specified . 15 | /// 16 | /// The assembly to scan 17 | public static void AddJson(params Assembly[] assemblies) => AddJson(new DommelJsonOptions { EntityAssemblies = assemblies }); 18 | 19 | /// 20 | /// Configures Dommel JSON support using the specified . 21 | /// 22 | /// 23 | public static void AddJson(DommelJsonOptions options) 24 | { 25 | if (options == null) 26 | { 27 | throw new ArgumentNullException(nameof(options)); 28 | } 29 | if (options.EntityAssemblies == null || options.EntityAssemblies.Length == 0) 30 | { 31 | throw new ArgumentException("No entity assemblies specified.", nameof(options)); 32 | } 33 | 34 | // Add SQL builders with JSON value support 35 | DommelMapper.AddSqlBuilder("sqlconnection", new SqlServerSqlBuilder()); 36 | DommelMapper.AddSqlBuilder("sqlceconnection", new SqlServerCeSqlBuilder()); 37 | DommelMapper.AddSqlBuilder("sqliteconnection", new SqliteSqlBuilder()); 38 | DommelMapper.AddSqlBuilder("npgsqlconnection", new PostgresSqlBuilder()); 39 | DommelMapper.AddSqlBuilder("mysqlconnection", new MySqlSqlBuilder()); 40 | 41 | // Add a custom SqlExpression factory with JSON support 42 | DommelMapper.SqlExpressionFactory = (type, sqlBuilder) => 43 | { 44 | if (sqlBuilder is not IJsonSqlBuilder) 45 | { 46 | throw new InvalidOperationException($"The specified SQL builder type should be assignable from {nameof(IJsonSqlBuilder)}."); 47 | } 48 | 49 | var sqlExpression = typeof(JsonSqlExpression<>).MakeGenericType(type); 50 | return Activator.CreateInstance(sqlExpression, sqlBuilder, options)!; 51 | }; 52 | 53 | // Add a Dapper type mapper with JSON support for 54 | // properties annotated with the [JsonData] attribute. 55 | var jsonTypeHander = options.JsonTypeHandler?.Invoke() ?? new JsonObjectTypeHandler(); 56 | var jsonTypes = new List(); 57 | foreach (var assembly in options.EntityAssemblies) 58 | { 59 | foreach (var type in assembly.ExportedTypes) 60 | { 61 | foreach (var property in type.GetRuntimeProperties()) 62 | { 63 | var jsonDataAttr = property.GetCustomAttribute(options.JsonDataAttributeType); 64 | if (jsonDataAttr != null) 65 | { 66 | SqlMapper.AddTypeHandler(property.PropertyType, jsonTypeHander); 67 | jsonTypes.Add(property.PropertyType); 68 | } 69 | } 70 | } 71 | } 72 | 73 | // Set a property resolver which considers the types discovered above 74 | // as primitive types so they will be used in insert and update queries. 75 | DommelMapper.SetPropertyResolver(new JsonPropertyResolver(jsonTypes)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Dommel.Json/DommelJsonOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Dapper; 4 | 5 | namespace Dommel.Json; 6 | 7 | /// 8 | /// Options for Dommel JSON support. 9 | /// 10 | public class DommelJsonOptions 11 | { 12 | /// 13 | /// Gets or sets the set of assemblies to scan for 14 | /// entities with [] properties. 15 | /// 16 | public Assembly[]? EntityAssemblies { get; set; } 17 | 18 | /// 19 | /// Gets or sets the Dapper type handler being used to handle JSON objects. 20 | /// 21 | public Func? JsonTypeHandler { get; set; } 22 | 23 | /// 24 | /// Gets or sets the type of the attribute which indicates that a property is a JSON data type. 25 | /// This defaults to . 26 | /// 27 | public Type JsonDataAttributeType { get; set; } = typeof(JsonDataAttribute); 28 | } 29 | -------------------------------------------------------------------------------- /src/Dommel.Json/IJsonSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Dommel.Json; 2 | 3 | /// 4 | /// Extends the with support for 5 | /// creating JSON value expressions. 6 | /// 7 | public interface IJsonSqlBuilder : ISqlBuilder 8 | { 9 | /// 10 | /// Creates a JSON value expression for the specified and . 11 | /// 12 | /// The column which contains the JSON data. 13 | /// The path of the JSON value to query. 14 | /// A JSON value expression. 15 | string JsonValue(string column, string path); 16 | } 17 | 18 | /// 19 | /// JSON SQL builder for SQL server. 20 | /// 21 | public class SqlServerSqlBuilder : Dommel.SqlServerSqlBuilder, IJsonSqlBuilder 22 | { 23 | /// 24 | public string JsonValue(string column, string path) => $"JSON_VALUE({column}, '$.{path}')"; 25 | } 26 | 27 | /// 28 | /// JSON SQL builder for MySQL. 29 | /// 30 | public class MySqlSqlBuilder : Dommel.MySqlSqlBuilder, IJsonSqlBuilder 31 | { 32 | /// 33 | public string JsonValue(string column, string path) => $"{column}->'$.{path}'"; 34 | } 35 | 36 | /// 37 | /// JSON SQL builder for PostgreSQL. 38 | /// 39 | public class PostgresSqlBuilder : Dommel.PostgresSqlBuilder, IJsonSqlBuilder 40 | { 41 | /// 42 | public string JsonValue(string column, string path) => $"{column}->>'{path}'"; 43 | } 44 | 45 | /// 46 | /// JSON SQL builder for SQLite. 47 | /// 48 | public class SqliteSqlBuilder : Dommel.SqliteSqlBuilder, IJsonSqlBuilder 49 | { 50 | /// 51 | public string JsonValue(string column, string path) => $"JSON_EXTRACT({column}, '$.{path}')"; 52 | } 53 | 54 | /// 55 | /// JSON SQL builder for SQL Server CE. 56 | /// 57 | public class SqlServerCeSqlBuilder : Dommel.SqlServerCeSqlBuilder, IJsonSqlBuilder 58 | { 59 | /// 60 | public string JsonValue(string column, string path) => $"JSON_VALUE({column}, '$.{path}')"; 61 | } 62 | -------------------------------------------------------------------------------- /src/Dommel.Json/JsonDataAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel.Json; 4 | 5 | /// 6 | /// Specifies that a property is persisted as a JSON document. 7 | /// 8 | [AttributeUsage(AttributeTargets.Property)] 9 | public class JsonDataAttribute : Attribute 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Dommel.Json/JsonObjectTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using Dapper; 6 | 7 | namespace Dommel.Json; 8 | 9 | internal class JsonObjectTypeHandler : SqlMapper.ITypeHandler 10 | { 11 | private static readonly JsonSerializerOptions JsonOptions = new() 12 | { 13 | AllowTrailingCommas = true, 14 | ReadCommentHandling = JsonCommentHandling.Skip, 15 | NumberHandling = JsonNumberHandling.AllowReadingFromString, 16 | }; 17 | 18 | public void SetValue(IDbDataParameter parameter, object? value) 19 | { 20 | parameter.Value = value is null || value is DBNull 21 | ? DBNull.Value 22 | : JsonSerializer.Serialize(value, JsonOptions); 23 | parameter.DbType = DbType.String; 24 | } 25 | 26 | public object? Parse(Type destinationType, object? value) => 27 | value is string str ? JsonSerializer.Deserialize(str, destinationType, JsonOptions) : null; 28 | } 29 | -------------------------------------------------------------------------------- /src/Dommel.Json/JsonPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Dommel.Json; 6 | 7 | internal class JsonPropertyResolver : DefaultPropertyResolver 8 | { 9 | private readonly HashSet _jsonPrimitiveTypes; 10 | 11 | public JsonPropertyResolver(IReadOnlyCollection jsonTypes) 12 | { 13 | // Append the given types to the base set of types Dommel considers 14 | // primitive so they will be used in insert and update queries. 15 | _jsonPrimitiveTypes = new HashSet(base.PrimitiveTypes.Concat(jsonTypes)); 16 | JsonTypes = jsonTypes; 17 | } 18 | 19 | // Internal for testing 20 | internal IReadOnlyCollection JsonTypes { get; } 21 | 22 | protected override HashSet PrimitiveTypes => _jsonPrimitiveTypes; 23 | } 24 | -------------------------------------------------------------------------------- /src/Dommel.Json/JsonSqlExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Reflection; 3 | 4 | namespace Dommel.Json; 5 | 6 | internal class JsonSqlExpression : SqlExpression 7 | where T : class 8 | { 9 | private readonly DommelJsonOptions _options; 10 | 11 | public JsonSqlExpression(IJsonSqlBuilder sqlBuilder, DommelJsonOptions options) : base(sqlBuilder) 12 | { 13 | _options = options; 14 | } 15 | 16 | public new IJsonSqlBuilder SqlBuilder => (IJsonSqlBuilder)base.SqlBuilder; 17 | 18 | protected override object VisitMemberAccess(MemberExpression expression) 19 | { 20 | if (expression.Member is PropertyInfo jsonValue && 21 | expression.Expression is MemberExpression jsonContainerExpr && 22 | jsonContainerExpr.Member is PropertyInfo jsonContainer && 23 | jsonContainer.IsDefined(_options.JsonDataAttributeType)) 24 | { 25 | return SqlBuilder.JsonValue( 26 | VisitMemberAccess(jsonContainerExpr).ToString()!, 27 | ColumnNameResolver.ResolveColumnName(jsonValue)); 28 | } 29 | 30 | return base.VisitMemberAccess(expression); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Dommel/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("Dommel.Tests")] 3 | [assembly: InternalsVisibleTo("Dommel.IntegrationTests")] 4 | [assembly: InternalsVisibleTo("Dommel.Json.Tests")] 5 | [assembly: InternalsVisibleTo("Dommel.Json.IntegrationTests")] -------------------------------------------------------------------------------- /src/Dommel/Cache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Dommel; 5 | 6 | internal enum QueryCacheType 7 | { 8 | Get, 9 | GetByMultipleIds, 10 | GetAll, 11 | Project, 12 | ProjectAll, 13 | Count, 14 | Insert, 15 | Update, 16 | Delete, 17 | DeleteAll, 18 | Any, 19 | } 20 | 21 | internal readonly struct QueryCacheKey : IEquatable 22 | { 23 | public QueryCacheKey(QueryCacheType cacheType, ISqlBuilder sqlBuilder, MemberInfo memberInfo) 24 | { 25 | SqlBuilderType = sqlBuilder.GetType(); 26 | CacheType = cacheType; 27 | MemberInfo = memberInfo; 28 | } 29 | 30 | public QueryCacheType CacheType { get; } 31 | 32 | public Type SqlBuilderType { get; } 33 | 34 | public MemberInfo MemberInfo { get; } 35 | 36 | public readonly bool Equals(QueryCacheKey other) => CacheType == other.CacheType && SqlBuilderType == other.SqlBuilderType && MemberInfo == other.MemberInfo; 37 | 38 | public override bool Equals(object? obj) => obj is QueryCacheKey key && Equals(key); 39 | 40 | public override int GetHashCode() => CacheType.GetHashCode() + SqlBuilderType.GetHashCode() + MemberInfo.GetHashCode(); 41 | } 42 | -------------------------------------------------------------------------------- /src/Dommel/ColumnPropertyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Reflection; 4 | 5 | namespace Dommel; 6 | 7 | /// 8 | /// Represents the column of an entity. 9 | /// 10 | public class ColumnPropertyInfo 11 | { 12 | /// 13 | /// Initializes a new instance from the 14 | /// specified instance. 15 | /// 16 | /// 17 | /// The property which represents the database column. The is 18 | /// determined from the option specified on 19 | /// the property. Defaults to when 20 | /// is true; otherwise, . 21 | /// 22 | /// Indicates whether a property is a key column. 23 | public ColumnPropertyInfo(PropertyInfo property, bool isKey = false) 24 | { 25 | Property = property ?? throw new ArgumentNullException(nameof(property)); 26 | GeneratedOption = property.GetCustomAttribute()?.DatabaseGeneratedOption 27 | ?? (isKey ? DatabaseGeneratedOption.Identity : DatabaseGeneratedOption.None); 28 | } 29 | 30 | /// 31 | /// Initializes a new instance from the 32 | /// specified instance using the specified 33 | /// . 34 | /// 35 | /// The property which represents the database column. 36 | /// 37 | /// The which specifies whether the value of 38 | /// the column this property represents is generated by the database. 39 | /// 40 | public ColumnPropertyInfo(PropertyInfo property, DatabaseGeneratedOption generatedOption) 41 | { 42 | Property = property ?? throw new ArgumentNullException(nameof(property)); 43 | GeneratedOption = generatedOption; 44 | } 45 | 46 | /// 47 | /// Gets a reference to the instance. 48 | /// 49 | public PropertyInfo Property { get; } 50 | 51 | /// 52 | /// Gets the which specifies whether the value of 53 | /// the column this property represents is generated by the database. 54 | /// 55 | public DatabaseGeneratedOption GeneratedOption { get; } 56 | 57 | /// 58 | /// Gets a value indicating whether this key property's value is generated by the database. 59 | /// 60 | public bool IsGenerated => GeneratedOption != DatabaseGeneratedOption.None; 61 | } 62 | -------------------------------------------------------------------------------- /src/Dommel/Count.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using Dapper; 6 | 7 | namespace Dommel; 8 | 9 | public static partial class DommelMapper 10 | { 11 | /// 12 | /// Returns the number of all entities. 13 | /// 14 | /// The type of the entity. 15 | /// The connection to the database. This can either be open or closed. 16 | /// Optional transaction for the command. 17 | /// The number of entities matching the specified predicate. 18 | public static long Count(this IDbConnection connection, IDbTransaction? transaction = null) 19 | { 20 | var sql = BuildCountAllSql(GetSqlBuilder(connection), typeof(TEntity)); 21 | LogQuery(sql); 22 | return connection.ExecuteScalar(sql, transaction); 23 | } 24 | 25 | /// 26 | /// Returns the number of all entities. 27 | /// 28 | /// The type of the entity. 29 | /// The connection to the database. This can either be open or closed. 30 | /// Optional transaction for the command. 31 | /// The number of entities matching the specified predicate. 32 | public static Task CountAsync(this IDbConnection connection, IDbTransaction? transaction = null) 33 | { 34 | var sql = BuildCountAllSql(GetSqlBuilder(connection), typeof(TEntity)); 35 | LogQuery(sql); 36 | return connection.ExecuteScalarAsync(sql, transaction); 37 | } 38 | 39 | /// 40 | /// Returns the number of entities matching the specified predicate. 41 | /// 42 | /// The type of the entity. 43 | /// The connection to the database. This can either be open or closed. 44 | /// A predicate to filter the results. 45 | /// Optional transaction for the command. 46 | /// The number of entities matching the specified predicate. 47 | public static long Count(this IDbConnection connection, Expression> predicate, IDbTransaction? transaction = null) 48 | { 49 | var sql = BuildCountSql(GetSqlBuilder(connection), predicate, out var parameters); 50 | LogQuery(sql); 51 | return connection.ExecuteScalar(sql, parameters, transaction); 52 | } 53 | 54 | /// 55 | /// Returns the number of entities matching the specified predicate. 56 | /// 57 | /// The type of the entity. 58 | /// The connection to the database. This can either be open or closed. 59 | /// A predicate to filter the results. 60 | /// Optional transaction for the command. 61 | /// The number of entities matching the specified predicate. 62 | public static Task CountAsync(this IDbConnection connection, Expression> predicate, IDbTransaction? transaction = null) 63 | { 64 | var sql = BuildCountSql(GetSqlBuilder(connection), predicate, out var parameters); 65 | LogQuery(sql); 66 | return connection.ExecuteScalarAsync(sql, parameters, transaction); 67 | } 68 | 69 | internal static string BuildCountAllSql(ISqlBuilder sqlBuilder, Type type) 70 | { 71 | var cacheKey = new QueryCacheKey(QueryCacheType.Count, sqlBuilder, type); 72 | if (!QueryCache.TryGetValue(cacheKey, out var sql)) 73 | { 74 | var tableName = Resolvers.Table(type, sqlBuilder); 75 | sql = $"select count(*) from {tableName}"; 76 | QueryCache.TryAdd(cacheKey, sql); 77 | } 78 | 79 | return sql; 80 | } 81 | 82 | internal static string BuildCountSql(ISqlBuilder sqlBuilder, Expression> predicate, out DynamicParameters parameters) 83 | { 84 | var sql = BuildCountAllSql(sqlBuilder, typeof(TEntity)); 85 | sql += CreateSqlExpression(sqlBuilder) 86 | .Where(predicate) 87 | .ToSql(out parameters); 88 | return sql; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Dommel/DateOnlyTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Dapper; 4 | 5 | namespace Dommel; 6 | 7 | #if NET6_0_OR_GREATER 8 | internal class DateOnlyTypeHandler : SqlMapper.TypeHandler 9 | { 10 | public override DateOnly Parse(object value) => DateOnly.FromDateTime((DateTime)value); 11 | 12 | public override void SetValue(IDbDataParameter parameter, DateOnly value) 13 | { 14 | parameter.DbType = DbType.Date; 15 | parameter.Value = value; 16 | } 17 | } 18 | 19 | internal class TimeOnlyTypeHandler : SqlMapper.TypeHandler 20 | { 21 | public override TimeOnly Parse(object value) => TimeOnly.FromDateTime((DateTime)value); 22 | 23 | public override void SetValue(IDbDataParameter parameter, TimeOnly value) 24 | { 25 | parameter.DbType = DbType.Time; 26 | parameter.Value = value; 27 | } 28 | } 29 | #endif -------------------------------------------------------------------------------- /src/Dommel/DefaultColumnNameResolver.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using System.Reflection; 3 | 4 | namespace Dommel; 5 | 6 | /// 7 | /// Implements the . 8 | /// 9 | public class DefaultColumnNameResolver : IColumnNameResolver 10 | { 11 | /// 12 | /// Resolves the column name for the property. 13 | /// Looks for the [Column] attribute. Otherwise it's just the name of the property. 14 | /// 15 | public virtual string ResolveColumnName(PropertyInfo propertyInfo) 16 | { 17 | var columnAttr = propertyInfo.GetCustomAttribute(); 18 | return columnAttr?.Name ?? propertyInfo.Name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Dommel/DefaultForeignKeyPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace Dommel; 8 | 9 | /// 10 | /// Implements the interface. 11 | /// 12 | public class DefaultForeignKeyPropertyResolver : IForeignKeyPropertyResolver 13 | { 14 | /// 15 | /// Resolves the foreign key property for the specified source type and including type 16 | /// by using + Id as property name. 17 | /// 18 | /// The source type which should contain the foreign key property. 19 | /// The type of the foreign key relation. 20 | /// The foreign key relationship type. 21 | /// The foreign key property for and . 22 | public virtual PropertyInfo ResolveForeignKeyProperty(Type sourceType, Type includingType, out ForeignKeyRelation foreignKeyRelation) 23 | { 24 | var oneToOneFk = ResolveOneToOne(sourceType, includingType); 25 | if (oneToOneFk != null) 26 | { 27 | foreignKeyRelation = ForeignKeyRelation.OneToOne; 28 | return oneToOneFk; 29 | } 30 | 31 | var oneToManyFk = ResolveOneToMany(sourceType, includingType); 32 | if (oneToManyFk != null) 33 | { 34 | foreignKeyRelation = ForeignKeyRelation.OneToMany; 35 | return oneToManyFk; 36 | } 37 | 38 | throw new InvalidOperationException( 39 | $"Could not resolve foreign key property. Source type '{sourceType.FullName}'; including type: '{includingType.FullName}'."); 40 | } 41 | 42 | private static PropertyInfo? ResolveOneToOne(Type sourceType, Type includingType) 43 | { 44 | // Look for the foreign key on the source type by making an educated guess about the property name. 45 | var foreignKeyName = includingType.Name + "Id"; 46 | var foreignKeyProperty = sourceType.GetProperty(foreignKeyName); 47 | if (foreignKeyProperty != null) 48 | { 49 | return foreignKeyProperty; 50 | } 51 | 52 | // Determine if the source type contains a navigation property to the including type. 53 | var navigationProperty = sourceType.GetProperties().FirstOrDefault(p => p.PropertyType == includingType); 54 | if (navigationProperty != null) 55 | { 56 | // Resolve the foreign key property from the attribute. 57 | var fkAttr = navigationProperty.GetCustomAttribute(); 58 | if (fkAttr != null) 59 | { 60 | return sourceType.GetProperty(fkAttr.Name); 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | private static PropertyInfo? ResolveOneToMany(Type sourceType, Type includingType) 68 | { 69 | // Look for the foreign key on the including type by making an educated guess about the property name. 70 | var foreignKeyName = sourceType.Name + "Id"; 71 | var foreignKeyProperty = includingType.GetProperty(foreignKeyName); 72 | if (foreignKeyProperty != null) 73 | { 74 | return foreignKeyProperty; 75 | } 76 | 77 | var collectionType = typeof(IEnumerable<>).MakeGenericType(includingType); 78 | var navigationProperty = sourceType.GetProperties().FirstOrDefault(p => collectionType.IsAssignableFrom(p.PropertyType)); 79 | if (navigationProperty != null) 80 | { 81 | // Resolve the foreign key property from the attribute. 82 | var fkAttr = navigationProperty.GetCustomAttribute(); 83 | if (fkAttr != null) 84 | { 85 | return includingType.GetProperty(fkAttr.Name); 86 | } 87 | } 88 | 89 | return null; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Dommel/DefaultKeyPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace Dommel; 7 | 8 | /// 9 | /// Implements the interface by resolving key properties 10 | /// with the [] or with the name 'Id'. 11 | /// 12 | public class DefaultKeyPropertyResolver : IKeyPropertyResolver 13 | { 14 | /// 15 | /// Finds the key properties by looking for properties with the 16 | /// [] attribute or with the name 'Id'. 17 | /// 18 | public ColumnPropertyInfo[] ResolveKeyProperties(Type type) 19 | { 20 | var keyProps = Resolvers 21 | .Properties(type) 22 | .Select(x => x.Property) 23 | .Where(p => string.Equals(p.Name, "Id", StringComparison.OrdinalIgnoreCase) || p.GetCustomAttribute() != null) 24 | .ToArray(); 25 | 26 | if (keyProps.Length == 0) 27 | { 28 | throw new InvalidOperationException($"Could not find the key properties for type '{type.FullName}'."); 29 | } 30 | 31 | return keyProps.Select(p => new ColumnPropertyInfo(p, isKey: true)).ToArray(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Dommel/DefaultPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Reflection; 5 | 6 | namespace Dommel; 7 | 8 | /// 9 | /// Default implemenation of the interface. 10 | /// 11 | public class DefaultPropertyResolver : IPropertyResolver 12 | { 13 | private static readonly HashSet PrimitiveTypesSet = new() 14 | { 15 | typeof(object), 16 | typeof(string), 17 | typeof(Guid), 18 | typeof(decimal), 19 | typeof(double), 20 | typeof(float), 21 | typeof(DateTime), 22 | typeof(DateTimeOffset), 23 | typeof(TimeSpan), 24 | typeof(byte[]), 25 | #if NET6_0_OR_GREATER 26 | typeof(DateOnly), 27 | typeof(TimeOnly), 28 | #endif 29 | }; 30 | 31 | /// 32 | /// Resolves the properties to be mapped for the specified type. 33 | /// 34 | /// The type to resolve the properties to be mapped for. 35 | /// A collection of 's of the . 36 | public virtual IEnumerable ResolveProperties(Type type) 37 | { 38 | foreach (var property in FilterComplexTypes(type.GetRuntimeProperties())) 39 | { 40 | if (!property.IsDefined(typeof(IgnoreAttribute)) && !property.IsDefined(typeof(NotMappedAttribute))) 41 | { 42 | yield return new ColumnPropertyInfo(property); 43 | } 44 | } 45 | } 46 | 47 | /// 48 | /// Gets a collection of types that are considered 'primitive' for Dommel but are not for the CLR. 49 | /// Override this to specify your own set of types. 50 | /// 51 | protected virtual HashSet PrimitiveTypes => PrimitiveTypesSet; 52 | 53 | /// 54 | /// Filters the complex types from the specified collection of properties. 55 | /// 56 | /// A collection of properties. 57 | /// The properties that are considered 'primitive' of . 58 | protected virtual IEnumerable FilterComplexTypes(IEnumerable properties) 59 | { 60 | foreach (var property in properties) 61 | { 62 | var type = property.PropertyType; 63 | type = Nullable.GetUnderlyingType(type) ?? type; 64 | if (type.GetTypeInfo().IsPrimitive || type.GetTypeInfo().IsEnum || PrimitiveTypes.Contains(type)) 65 | { 66 | yield return property; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Dommel/DefaultTableNameResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Reflection; 4 | 5 | namespace Dommel; 6 | 7 | /// 8 | /// Default implementation of the interface. 9 | /// 10 | public class DefaultTableNameResolver : ITableNameResolver 11 | { 12 | /// 13 | /// Resolves the table name. 14 | /// Looks for the [Table] attribute. Otherwise by making the type 15 | /// plural (eg. Product -> Products) and removing the 'I' prefix for interfaces. 16 | /// 17 | public virtual string ResolveTableName(Type type) 18 | { 19 | var typeInfo = type.GetTypeInfo(); 20 | var tableAttr = typeInfo.GetCustomAttribute(); 21 | if (tableAttr != null) 22 | { 23 | if (!string.IsNullOrEmpty(tableAttr.Schema)) 24 | { 25 | return $"{tableAttr.Schema}.{tableAttr.Name}"; 26 | } 27 | 28 | return tableAttr.Name; 29 | } 30 | 31 | // Fall back to plural of table name 32 | var name = type.Name; 33 | if (name.EndsWith("y", StringComparison.OrdinalIgnoreCase)) 34 | { 35 | // Category -> Categories 36 | name = name.Remove(name.Length - 1) + "ies"; 37 | } 38 | else if (!name.EndsWith("s", StringComparison.OrdinalIgnoreCase)) 39 | { 40 | // Product -> Products 41 | name += "s"; 42 | } 43 | 44 | return name; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Dommel/Dommel.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net8.0;net9.0 4 | Simple CRUD operations for Dapper. 5 | dommel;crud;dapper;database;orm 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Dommel/ForeignKeyRelation.cs: -------------------------------------------------------------------------------- 1 | namespace Dommel; 2 | 3 | /// 4 | /// Describes a foreign key relationship. 5 | /// 6 | public enum ForeignKeyRelation 7 | { 8 | /// 9 | /// Specifies a one-to-one relationship. 10 | /// 11 | OneToOne, 12 | 13 | /// 14 | /// Specifies a one-to-many relationship. 15 | /// 16 | OneToMany 17 | } 18 | -------------------------------------------------------------------------------- /src/Dommel/From.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Threading.Tasks; 5 | using Dapper; 6 | 7 | namespace Dommel; 8 | 9 | public static partial class DommelMapper 10 | { 11 | /// 12 | /// Executes an expression to query data from . 13 | /// 14 | /// The entity to query data from. 15 | /// The connection to query data from. 16 | /// A callback to build a . 17 | /// Optional transaction for the command. 18 | /// 19 | /// A value indicating whether the result of the query should be executed directly, 20 | /// or when the query is materialized (using ToList() for example). 21 | /// 22 | /// The collection of entities returned from the query. 23 | public static IEnumerable From(this IDbConnection con, Action> sqlBuilder, IDbTransaction? transaction = null, bool buffered = true) 24 | { 25 | var sqlExpression = CreateSqlExpression(GetSqlBuilder(con)); 26 | sqlBuilder(sqlExpression); 27 | var sql = sqlExpression.ToSql(out var parameters); 28 | LogReceived?.Invoke(sql); 29 | return con.Query(sql, parameters, transaction, buffered); 30 | } 31 | 32 | /// 33 | /// Executes an expression to query data from . 34 | /// 35 | /// The entity to query data from. 36 | /// The connection to query data from. 37 | /// A callback to build a . 38 | /// Optional transaction for the command. 39 | /// The collection of entities returned from the query. 40 | public static async Task> FromAsync(this IDbConnection con, Action> sqlBuilder, IDbTransaction? transaction = null) 41 | { 42 | var sqlExpression = CreateSqlExpression(GetSqlBuilder(con)); 43 | sqlBuilder(sqlExpression); 44 | var sql = sqlExpression.ToSql(out var parameters); 45 | LogReceived?.Invoke(sql); 46 | return await con.QueryAsync(sql, parameters, transaction); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Dommel/IColumnNameResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// Defines methods for resolving column names for entities. 7 | /// Custom implementations can be registered with . 8 | /// 9 | public interface IColumnNameResolver 10 | { 11 | /// 12 | /// Resolves the column name for the specified property. 13 | /// 14 | /// The property of the entity. 15 | /// The column name for the property. 16 | string ResolveColumnName(PropertyInfo propertyInfo); 17 | } 18 | -------------------------------------------------------------------------------- /src/Dommel/IForeignKeyPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Dommel; 5 | 6 | /// 7 | /// Defines methods for resolving foreign key properties. 8 | /// Custom implementations can be registered with . 9 | /// 10 | public interface IForeignKeyPropertyResolver 11 | { 12 | /// 13 | /// Resolves the foreign key property for the specified source type and including type. 14 | /// 15 | /// The source type which should contain the foreign key property. 16 | /// The type of the foreign key relation. 17 | /// The foreign key relationship type. 18 | /// The foreign key property for and . 19 | PropertyInfo ResolveForeignKeyProperty(Type sourceType, Type includingType, out ForeignKeyRelation foreignKeyRelation); 20 | } 21 | -------------------------------------------------------------------------------- /src/Dommel/IKeyPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Dommel; 5 | 6 | /// 7 | /// Defines methods for resolving the key property of entities. 8 | /// Custom implementations can be registered with . 9 | /// 10 | public interface IKeyPropertyResolver 11 | { 12 | /// 13 | /// Resolves the key properties for the specified type. 14 | /// 15 | /// The type to resolve the key properties for. 16 | /// A collection of instances of the key properties of . 17 | ColumnPropertyInfo[] ResolveKeyProperties(Type type); 18 | } 19 | -------------------------------------------------------------------------------- /src/Dommel/IPropertyResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace Dommel; 6 | 7 | /// 8 | /// Defines methods for resolving the properties of entities. 9 | /// Custom implementations can be registered with . 10 | /// 11 | public interface IPropertyResolver 12 | { 13 | /// 14 | /// Resolves the properties to be mapped for the specified type. 15 | /// 16 | /// The type to resolve the properties to be mapped for. 17 | /// A collection of 's of the . 18 | IEnumerable ResolveProperties(Type type); 19 | } 20 | -------------------------------------------------------------------------------- /src/Dommel/ISqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// Defines methods for building specialized SQL queries. 7 | /// 8 | public interface ISqlBuilder 9 | { 10 | /// 11 | /// Adds a prefix to the specified parameter. 12 | /// 13 | /// The name of the parameter to prefix. 14 | string PrefixParameter(string paramName); 15 | 16 | /// 17 | /// Builds an insert query using the specified table name, column names and parameter names. 18 | /// A query to fetch the new ID will be included as well. 19 | /// 20 | /// The type of the entity to generate the insert query for. 21 | /// The name of the table to query. 22 | /// The names of the columns in the table. 23 | /// The names of the parameters in the database command. 24 | /// An insert query including a query to fetch the new ID. 25 | string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames); 26 | 27 | /// 28 | /// Builds the paging part to be appended to an existing select query. 29 | /// 30 | /// The order by part of the query. 31 | /// The number of the page to fetch, starting at 1. 32 | /// The page size. 33 | /// The paging part of a query. 34 | string BuildPaging(string? orderBy, int pageNumber, int pageSize); 35 | 36 | /// 37 | /// Adds quotes around (or at the start) of an identifier such as a table or column name. 38 | /// 39 | /// The identifier add quotes around. E.g. a table or column name. 40 | /// The quoted . 41 | string QuoteIdentifier(string identifier); 42 | 43 | /// 44 | /// Returns a limit clause for the specified . 45 | /// 46 | /// The count of limit clause. 47 | /// A limit clause of the specified count. 48 | string LimitClause(int count); 49 | 50 | /// 51 | /// Returns a like-expresion for the specified and . 52 | /// 53 | /// The column name of the like-expression. 54 | /// The parameter name of the like-expression. 55 | /// A like-expression. 56 | string LikeExpression(string columnName, string parameterName); 57 | } 58 | -------------------------------------------------------------------------------- /src/Dommel/ITableNameResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// Defines methods for resolving table names of entities. 7 | /// Custom implementations can be registered with . 8 | /// 9 | public interface ITableNameResolver 10 | { 11 | /// 12 | /// Resolves the table name for the specified type. 13 | /// 14 | /// The type to resolve the table name for. 15 | /// A string containing the resolved table name for for . 16 | string ResolveTableName(Type type); 17 | } 18 | -------------------------------------------------------------------------------- /src/Dommel/IgnoreAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// Specifies that a property should be ignored. 7 | /// 8 | [AttributeUsage(AttributeTargets.Property)] 9 | public class IgnoreAttribute : Attribute 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Dommel/MySqlSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// implementation for MySQL. 7 | /// 8 | public class MySqlSqlBuilder : ISqlBuilder 9 | { 10 | /// 11 | public virtual string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) => 12 | $"insert into {tableName} ({string.Join(", ", columnNames)}) values ({string.Join(", ", paramNames)}); select LAST_INSERT_ID() id"; 13 | 14 | /// 15 | public virtual string BuildPaging(string? orderBy, int pageNumber, int pageSize) 16 | { 17 | var start = pageNumber >= 1 ? (pageNumber - 1) * pageSize : 0; 18 | return $" {orderBy} limit {start}, {pageSize}"; 19 | } 20 | 21 | /// 22 | public string PrefixParameter(string paramName) => $"@{paramName}"; 23 | 24 | /// 25 | public string QuoteIdentifier(string identifier) => $"`{identifier}`"; 26 | 27 | /// 28 | public string LimitClause(int count) => $"limit {count}"; 29 | 30 | /// 31 | public string LikeExpression(string columnName, string parameterName) => $"{columnName} like {parameterName}"; 32 | } 33 | -------------------------------------------------------------------------------- /src/Dommel/PostgresSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Dommel; 5 | 6 | /// 7 | /// implementation for Postgres. 8 | /// 9 | public class PostgresSqlBuilder : ISqlBuilder 10 | { 11 | /// 12 | public virtual string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) 13 | { 14 | if (type == null) 15 | { 16 | throw new ArgumentNullException(nameof(type)); 17 | } 18 | 19 | var sql = $"insert into {tableName} ({string.Join(", ", columnNames)}) values ({string.Join(", ", paramNames)}) "; 20 | 21 | var keyColumns = Resolvers.KeyProperties(type).Where(p => p.IsGenerated).Select(p => Resolvers.Column(p.Property, this, false)); 22 | if (keyColumns.Any()) 23 | { 24 | sql += $"returning ({string.Join(", ", keyColumns)})"; 25 | } 26 | 27 | return sql; 28 | } 29 | 30 | /// 31 | public virtual string BuildPaging(string? orderBy, int pageNumber, int pageSize) 32 | { 33 | var start = pageNumber >= 1 ? (pageNumber - 1) * pageSize : 0; 34 | return $" {orderBy} offset {start} limit {pageSize}"; 35 | } 36 | 37 | /// 38 | public string PrefixParameter(string paramName) => $"@{paramName}"; 39 | 40 | /// 41 | public string QuoteIdentifier(string identifier) => $"\"{identifier}\""; 42 | 43 | /// 44 | public string LimitClause(int count) => $"limit {count}"; 45 | 46 | /// 47 | public string LikeExpression(string columnName, string parameterName) => $"{columnName} ilike {parameterName}"; 48 | } 49 | -------------------------------------------------------------------------------- /src/Dommel/SqlServerCeSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// implementation for SQL Server Compact Edition. 7 | /// 8 | public class SqlServerCeSqlBuilder : ISqlBuilder 9 | { 10 | /// 11 | public virtual string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) => 12 | $"insert into {tableName} ({string.Join(", ", columnNames)}) values ({string.Join(", ", paramNames)}); select @@IDENTITY"; 13 | 14 | /// 15 | public virtual string BuildPaging(string? orderBy, int pageNumber, int pageSize) 16 | { 17 | var start = pageNumber >= 1 ? (pageNumber - 1) * pageSize : 0; 18 | return $" {orderBy} offset {start} rows fetch next {pageSize} rows only"; 19 | } 20 | 21 | /// 22 | public string PrefixParameter(string paramName) => $"@{paramName}"; 23 | 24 | /// 25 | public string QuoteIdentifier(string identifier) => $"[{identifier}]"; 26 | 27 | /// 28 | public string LimitClause(int count) => $"order by 1 offset 0 rows fetch next {count} rows only"; 29 | 30 | /// 31 | public string LikeExpression(string columnName, string parameterName) => $"{columnName} like {parameterName}"; 32 | } 33 | -------------------------------------------------------------------------------- /src/Dommel/SqlServerSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// implementation for SQL Server. 7 | /// 8 | public class SqlServerSqlBuilder : ISqlBuilder 9 | { 10 | /// 11 | public virtual string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) => 12 | $"set nocount on insert into {tableName} ({string.Join(", ", columnNames)}) values ({string.Join(", ", paramNames)}); select scope_identity()"; 13 | 14 | /// 15 | public virtual string BuildPaging(string? orderBy, int pageNumber, int pageSize) 16 | { 17 | var start = pageNumber >= 1 ? (pageNumber - 1) * pageSize : 0; 18 | return $" {orderBy} offset {start} rows fetch next {pageSize} rows only"; 19 | } 20 | 21 | /// 22 | public string PrefixParameter(string paramName) => $"@{paramName}"; 23 | 24 | /// 25 | public string QuoteIdentifier(string identifier) => $"[{identifier}]"; 26 | 27 | /// 28 | public string LimitClause(int count) => $"order by 1 offset 0 rows fetch next {count} rows only"; 29 | 30 | /// 31 | public string LikeExpression(string columnName, string parameterName) => $"{columnName} like {parameterName}"; 32 | } 33 | -------------------------------------------------------------------------------- /src/Dommel/SqliteSqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel; 4 | 5 | /// 6 | /// implementation for SQLite. 7 | /// 8 | public class SqliteSqlBuilder : ISqlBuilder 9 | { 10 | /// 11 | public virtual string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) => 12 | $"insert into {tableName} ({string.Join(", ", columnNames)}) values ({string.Join(", ", paramNames)}); select last_insert_rowid() id"; 13 | 14 | /// 15 | public virtual string BuildPaging(string? orderBy, int pageNumber, int pageSize) 16 | { 17 | var start = pageNumber >= 1 ? (pageNumber - 1) * pageSize : 0; 18 | return $" {orderBy} LIMIT {start}, {pageSize}"; 19 | } 20 | 21 | /// 22 | public string PrefixParameter(string paramName) => $"@{paramName}"; 23 | 24 | /// 25 | public string QuoteIdentifier(string identifier) => identifier; 26 | 27 | /// 28 | public string LimitClause(int count) => $"limit {count}"; 29 | 30 | /// 31 | public string LikeExpression(string columnName, string parameterName) => $"{columnName} like {parameterName}"; 32 | } 33 | -------------------------------------------------------------------------------- /src/Dommel/Update.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Dapper; 7 | 8 | namespace Dommel; 9 | 10 | public static partial class DommelMapper 11 | { 12 | /// 13 | /// Updates the values of the specified entity in the database. 14 | /// The return value indicates whether the operation succeeded. 15 | /// 16 | /// The type of the entity. 17 | /// The connection to the database. This can either be open or closed. 18 | /// The entity in the database. 19 | /// Optional transaction for the command. 20 | /// A value indicating whether the update operation succeeded. 21 | public static bool Update(this IDbConnection connection, TEntity entity, IDbTransaction? transaction = null) 22 | { 23 | var sql = BuildUpdateQuery(GetSqlBuilder(connection), typeof(TEntity)); 24 | LogQuery(sql); 25 | return connection.Execute(sql, entity, transaction) > 0; 26 | } 27 | 28 | /// 29 | /// Updates the values of the specified entity in the database. 30 | /// The return value indicates whether the operation succeeded. 31 | /// 32 | /// The type of the entity. 33 | /// The connection to the database. This can either be open or closed. 34 | /// The entity in the database. 35 | /// Optional transaction for the command. 36 | /// Optional cancellation token for the command. 37 | /// A value indicating whether the update operation succeeded. 38 | public static async Task UpdateAsync(this IDbConnection connection, TEntity entity, IDbTransaction? transaction = null, CancellationToken cancellationToken = default) 39 | { 40 | var sql = BuildUpdateQuery(GetSqlBuilder(connection), typeof(TEntity)); 41 | LogQuery(sql); 42 | return await connection.ExecuteAsync(new CommandDefinition(sql, entity, transaction: transaction, cancellationToken: cancellationToken)) > 0; 43 | } 44 | 45 | internal static string BuildUpdateQuery(ISqlBuilder sqlBuilder, Type type) 46 | { 47 | var cacheKey = new QueryCacheKey(QueryCacheType.Update, sqlBuilder, type); 48 | if (!QueryCache.TryGetValue(cacheKey, out var sql)) 49 | { 50 | var tableName = Resolvers.Table(type, sqlBuilder); 51 | 52 | // Use all non-key and non-generated properties for updates 53 | var keyProperties = Resolvers.KeyProperties(type); 54 | var typeProperties = Resolvers.Properties(type) 55 | .Where(x => !x.IsGenerated) 56 | .Select(x => x.Property) 57 | .Except(keyProperties.Where(p => p.IsGenerated).Select(p => p.Property)); 58 | 59 | var columnNames = typeProperties.Select(p => $"{Resolvers.Column(p, sqlBuilder, false)} = {sqlBuilder.PrefixParameter(p.Name)}").ToArray(); 60 | var whereClauses = keyProperties.Select(p => $"{Resolvers.Column(p.Property, sqlBuilder, false)} = {sqlBuilder.PrefixParameter(p.Property.Name)}"); 61 | sql = $"update {tableName} set {string.Join(", ", columnNames)} where {string.Join(" and ", whereClauses)}"; 62 | 63 | QueryCache.TryAdd(cacheKey, sql); 64 | } 65 | 66 | return sql; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/AnyTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class AnyTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void Any(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | Assert.True(con.Any()); 15 | } 16 | 17 | [Theory] 18 | [ClassData(typeof(DatabaseTestData))] 19 | public async Task AnyAsync(DatabaseDriver database) 20 | { 21 | using var con = database.GetConnection(); 22 | Assert.True(await con.AnyAsync()); 23 | } 24 | 25 | [Theory] 26 | [ClassData(typeof(DatabaseTestData))] 27 | public void AnyWithPredicate(DatabaseDriver database) 28 | { 29 | using var con = database.GetConnection(); 30 | Assert.True(con.Any(x => x.Name != null)); 31 | } 32 | 33 | [Theory] 34 | [ClassData(typeof(DatabaseTestData))] 35 | public async Task AnyAsyncWithPredicate(DatabaseDriver database) 36 | { 37 | using var con = database.GetConnection(); 38 | Assert.True(await con.AnyAsync(x => x.Name != null)); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/CountTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class CountTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void Count(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | Assert.True(con.Count() > 0); 15 | } 16 | 17 | [Theory] 18 | [ClassData(typeof(DatabaseTestData))] 19 | public async Task CountAsync(DatabaseDriver database) 20 | { 21 | using var con = database.GetConnection(); 22 | Assert.True(await con.CountAsync() > 0); 23 | } 24 | 25 | [Theory] 26 | [ClassData(typeof(DatabaseTestData))] 27 | public void CountWithPredicate(DatabaseDriver database) 28 | { 29 | using var con = database.GetConnection(); 30 | Assert.True(con.Count(x => x.Name != null) > 0); 31 | } 32 | 33 | [Theory] 34 | [ClassData(typeof(DatabaseTestData))] 35 | public async Task CountAsyncWithPredicate(DatabaseDriver database) 36 | { 37 | using var con = database.GetConnection(); 38 | Assert.True(await con.CountAsync(x => x.Name != null) > 0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Databases/DatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Dapper; 7 | 8 | namespace Dommel.IntegrationTests; 9 | 10 | /// 11 | /// Provides a driver to interact with a specific database system. 12 | /// 13 | public abstract class DatabaseDriver 14 | { 15 | public abstract string TempDbDatabaseName { get; } 16 | 17 | public virtual string DefaultDatabaseName => "dommeltests"; 18 | 19 | public abstract DbConnection GetConnection(string databaseName); 20 | 21 | public DbConnection GetConnection() => GetConnection(DefaultDatabaseName); 22 | 23 | public virtual async Task InitializeAsync() 24 | { 25 | await CreateDatabase(); 26 | var created = await CreateTables(); 27 | 28 | // Is the table created? If so, insert dummy data 29 | if (created) 30 | { 31 | using var connection = GetConnection(); 32 | await connection.OpenAsync(); 33 | 34 | var categoryId1 = Convert.ToInt32(await connection.InsertAsync(new Category { Name = "Food" })); 35 | var categoryId2 = Convert.ToInt32(await connection.InsertAsync(new Category { Name = "Food 2" })); 36 | 37 | var products = new List 38 | { 39 | new Product { CategoryId = categoryId1, Name = "Chai" }, 40 | new Product { CategoryId = categoryId1, Name = "Chang" }, 41 | new Product { CategoryId = categoryId1, Name = "Aniseed Syrup" }, 42 | new Product { CategoryId = categoryId1, Name = "Chef Anton's Cajun Seasoning" }, 43 | new Product { CategoryId = categoryId1, Name = "Chef Anton's Gumbo Mix" }, 44 | 45 | new Product { CategoryId = categoryId2, Name = "Chai 2" }, 46 | new Product { CategoryId = categoryId2, Name = "Chang 2" }, 47 | new Product { CategoryId = categoryId2, Name = "Aniseed Syrup 2" }, 48 | new Product { CategoryId = categoryId2, Name = "Chef Anton's Cajun Seasoning 2" }, 49 | new Product { CategoryId = categoryId2, Name = "Chef Anton's Gumbo Mix 2" }, 50 | 51 | new Product { Name = "Foo" }, // 11 52 | new Product { Name = "Bar" }, // 12 53 | new Product { Name = "Baz" }, // 13 54 | }; 55 | 56 | await connection.InsertAllAsync(products); 57 | 58 | var productId = (await connection.FirstOrDefaultAsync(x => x.Name == "Chai"))!.ProductId; 59 | await connection.InsertAsync(new ProductOption { ProductId = productId }); 60 | 61 | // Order 1 62 | var orderId = Convert.ToInt32(await connection.InsertAsync(new Order { Created = new DateTime(2011, 1, 1) })); 63 | var orderLines = new List 64 | { 65 | new OrderLine { OrderId = orderId, Line = "Line 1"}, 66 | new OrderLine { OrderId = orderId, Line = "Line 2"}, 67 | new OrderLine { OrderId = orderId, Line = "Line 3"}, 68 | }; 69 | await connection.InsertAllAsync(orderLines); 70 | 71 | // Order 2 72 | _ = await connection.InsertAsync(new Order { Created = new DateTime(2012, 2, 2) }); 73 | 74 | // Foo's and Bar's for delete queries 75 | await connection.InsertAllAsync(Enumerable.Range(0, 5).Select(_ => new Foo())); 76 | await connection.InsertAllAsync(Enumerable.Range(0, 5).Select(_ => new Bar())); 77 | 78 | // Composite key entities 79 | await connection.InsertAsync(new ProductsCategories { ProductId = 1, CategoryId = 1 }); 80 | await connection.InsertAsync(new ProductsCategories { ProductId = 1, CategoryId = 2 }); 81 | await connection.InsertAsync(new ProductsCategories { ProductId = 3, CategoryId = 1 }); 82 | } 83 | } 84 | 85 | protected abstract Task CreateDatabase(); 86 | 87 | protected abstract Task CreateTables(); 88 | 89 | protected virtual async Task DropTables() 90 | { 91 | using var con = GetConnection(DefaultDatabaseName); 92 | var sqlBuilder = DommelMapper.GetSqlBuilder(con); 93 | string Quote(string s) => sqlBuilder.QuoteIdentifier(s); 94 | 95 | await con.ExecuteAsync($@" 96 | DROP TABLE {Quote("Categories")}; 97 | DROP TABLE {Quote("Products")}; 98 | DROP TABLE {Quote("ProductsCategories")}; 99 | DROP TABLE {Quote("ProductOptions")}; 100 | DROP TABLE {Quote("Orders")}; 101 | DROP TABLE {Quote("OrderLines")}; 102 | DROP TABLE {Quote("Foos")}; 103 | DROP TABLE {Quote("Bars")}; 104 | DROP TABLE {Quote("Bazs")};"); 105 | } 106 | 107 | public virtual async Task DisposeAsync() => await DropTables(); 108 | } 109 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Databases/MySqlDatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Threading.Tasks; 3 | using Dapper; 4 | using MySqlConnector; 5 | 6 | namespace Dommel.IntegrationTests; 7 | 8 | public class MySqlDatabaseDriver : DatabaseDriver 9 | { 10 | public override DbConnection GetConnection(string databaseName) 11 | { 12 | var connectionString = $"Server=localhost;Database={databaseName};Uid=dommeltest;Pwd=test;"; 13 | if (CI.IsAppVeyor) 14 | { 15 | connectionString = $"Server=localhost;Database={databaseName};Uid=root;Pwd=Password12!;"; 16 | } 17 | else if (CI.IsTravis) 18 | { 19 | connectionString = $"Server=localhost;Database={databaseName};Uid=root;Pwd=;"; 20 | } 21 | 22 | return new MySqlConnection(connectionString); 23 | } 24 | 25 | public override string TempDbDatabaseName => "mysql"; 26 | 27 | protected override async Task CreateDatabase() 28 | { 29 | using var con = GetConnection(TempDbDatabaseName); 30 | await con.ExecuteAsync($"CREATE DATABASE IF NOT EXISTS {DefaultDatabaseName}"); 31 | } 32 | 33 | protected override async Task CreateTables() 34 | { 35 | using var con = GetConnection(DefaultDatabaseName); 36 | var sql = @" 37 | SELECT * FROM information_schema.tables where table_name = 'Products' LIMIT 1; 38 | CREATE TABLE IF NOT EXISTS Categories (CategoryId INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255)); 39 | CREATE TABLE IF NOT EXISTS ProductsCategories (ProductId INT, CategoryId INT, PRIMARY KEY (ProductId, CategoryId)); 40 | CREATE TABLE IF NOT EXISTS Products (ProductId INT AUTO_INCREMENT PRIMARY KEY, CategoryId int, FullName VARCHAR(255), Slug VARCHAR(255)); 41 | CREATE TABLE IF NOT EXISTS ProductOptions (Id INT AUTO_INCREMENT PRIMARY KEY, ProductId INT); 42 | CREATE TABLE IF NOT EXISTS Orders (Id INT AUTO_INCREMENT PRIMARY KEY, Created DATETIME NOT NULL); 43 | CREATE TABLE IF NOT EXISTS OrderLines (Id INT AUTO_INCREMENT PRIMARY KEY, OrderId int, Line VARCHAR(255)); 44 | CREATE TABLE IF NOT EXISTS Foos (Id INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255)); 45 | CREATE TABLE IF NOT EXISTS Bars (Id INT AUTO_INCREMENT PRIMARY KEY, Name VARCHAR(255)); 46 | CREATE TABLE IF NOT EXISTS Bazs (BazId CHAR(36) PRIMARY KEY, Name VARCHAR(255));"; 47 | var created = await con.ExecuteScalarAsync(sql); 48 | 49 | // No result means the tables were just created 50 | return created == null; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Databases/PostgresDatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Threading.Tasks; 3 | using Dapper; 4 | using Npgsql; 5 | 6 | namespace Dommel.IntegrationTests; 7 | 8 | public class PostgresDatabaseDriver : DatabaseDriver 9 | { 10 | public override DbConnection GetConnection(string databaseName) 11 | { 12 | var connectionString = $"Server=localhost;Port=5432;Database={databaseName};Uid=postgres;Pwd=postgres;"; 13 | if (CI.IsAppVeyor) 14 | { 15 | connectionString = $"Server=localhost;Port=5432;Database={databaseName};Uid=postgres;Pwd=Password12!;"; 16 | } 17 | else if (CI.IsTravis) 18 | { 19 | connectionString = $"Server=localhost;Port=5432;Database={databaseName};Uid=postgres;Pwd=;"; 20 | } 21 | 22 | return new NpgsqlConnection(connectionString); 23 | } 24 | 25 | public override string TempDbDatabaseName => "postgres"; 26 | 27 | protected override async Task CreateDatabase() 28 | { 29 | using var con = GetConnection(TempDbDatabaseName); 30 | try 31 | { 32 | // Always try to create the database as you'll run into 33 | // race conditions when tests run in parallel. 34 | await con.ExecuteAsync($"CREATE DATABASE {DefaultDatabaseName}"); 35 | } 36 | catch (PostgresException pex) when (pex.SqlState == "42P04") 37 | { 38 | // Ignore errors that the database already exists 39 | } 40 | } 41 | 42 | protected override async Task CreateTables() 43 | { 44 | using var con = GetConnection(DefaultDatabaseName); 45 | var sql = @" 46 | SELECT * FROM information_schema.tables WHERE table_name = 'Products' LIMIT 1; 47 | CREATE TABLE IF NOT EXISTS ""Categories"" (""CategoryId"" SERIAL PRIMARY KEY, ""Name"" VARCHAR(255)); 48 | CREATE TABLE IF NOT EXISTS ""Products"" (""ProductId"" SERIAL PRIMARY KEY, ""CategoryId"" INT, ""FullName"" VARCHAR(255), ""Slug"" VARCHAR(255)); 49 | CREATE TABLE IF NOT EXISTS ""ProductOptions"" (""Id"" SERIAL PRIMARY KEY, ""ProductId"" INT); 50 | CREATE TABLE IF NOT EXISTS ""ProductsCategories"" (""ProductId"" INT, ""CategoryId"" INT, PRIMARY KEY (""ProductId"", ""CategoryId"")); 51 | CREATE TABLE IF NOT EXISTS ""Orders"" (""Id"" SERIAL PRIMARY KEY, ""Created"" TIMESTAMP NOT NULL); 52 | CREATE TABLE IF NOT EXISTS ""OrderLines"" (""Id"" SERIAL PRIMARY KEY, ""OrderId"" INT, ""Line"" VARCHAR(255)); 53 | CREATE TABLE IF NOT EXISTS ""Foos"" (""Id"" SERIAL PRIMARY KEY, ""Name"" VARCHAR(255)); 54 | CREATE TABLE IF NOT EXISTS ""Bars"" (""Id"" SERIAL PRIMARY KEY, ""Name"" VARCHAR(255)); 55 | CREATE TABLE IF NOT EXISTS ""Bazs"" (""BazId"" UUID primary key, ""Name"" VARCHAR(255));"; 56 | var created = await con.ExecuteScalarAsync(sql); 57 | 58 | // No result means the tables were just created 59 | return created == null; 60 | } 61 | } -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Databases/SqlServerDatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Threading.Tasks; 3 | using Dapper; 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace Dommel.IntegrationTests; 7 | 8 | public class SqlServerDatabaseDriver : DatabaseDriver 9 | { 10 | public override DbConnection GetConnection(string databaseName) 11 | { 12 | var connectionString = CI.IsAppVeyor 13 | ? $"Server=(local)\\SQL2019;Database={databaseName};User ID=sa;Password=Password12!;Encrypt=False" 14 | : $"Server=(LocalDb)\\mssqllocaldb;Database={databaseName};User ID=dommel;Password=dommel;Encrypt=False"; 15 | 16 | return new SqlConnection(connectionString); 17 | } 18 | 19 | public override string TempDbDatabaseName => "tempdb"; 20 | 21 | protected override async Task CreateDatabase() 22 | { 23 | using var con = GetConnection(TempDbDatabaseName); 24 | await con.ExecuteAsync($"IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = N'{DefaultDatabaseName}') BEGIN CREATE DATABASE {DefaultDatabaseName}; END;"); 25 | } 26 | 27 | protected override async Task CreateTables() 28 | { 29 | using var con = GetConnection(DefaultDatabaseName); 30 | var sql = @"IF OBJECT_ID(N'dbo.Products', N'U') IS NULL 31 | BEGIN 32 | CREATE TABLE dbo.Categories (CategoryId INT IDENTITY(1,1) PRIMARY KEY, Name VARCHAR(255)); 33 | CREATE TABLE dbo.Products (ProductId INT IDENTITY(1,1) PRIMARY KEY, CategoryId int, FullName VARCHAR(255), Slug VARCHAR(255)); 34 | CREATE TABLE dbo.ProductOptions (Id INT IDENTITY(1,1) PRIMARY KEY, ProductId INT); 35 | CREATE TABLE dbo.ProductsCategories (ProductId INT, CategoryId INT, PRIMARY KEY (ProductId, CategoryId)); 36 | CREATE TABLE dbo.Orders (Id INT IDENTITY(1,1) PRIMARY KEY, Created DATETIME NOT NULL); 37 | CREATE TABLE dbo.OrderLines (Id INT IDENTITY(1,1) PRIMARY KEY, OrderId int, Line VARCHAR(255)); 38 | CREATE TABLE dbo.Foos (Id INT IDENTITY(1,1) PRIMARY KEY, Name VARCHAR(255)); 39 | CREATE TABLE dbo.Bars (Id INT IDENTITY(1,1) PRIMARY KEY, Name VARCHAR(255)); 40 | CREATE TABLE dbo.Bazs (BazId UNIQUEIDENTIFIER PRIMARY KEY, Name VARCHAR(255)); 41 | SELECT 1; 42 | END"; 43 | var created = await con.ExecuteScalarAsync(sql); 44 | 45 | // A result means the tables were just created 46 | return created != null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/DeleteTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Dommel.IntegrationTests; 8 | 9 | [Collection("Database")] 10 | public class DeleteTests 11 | { 12 | [Theory] 13 | [ClassData(typeof(DatabaseTestData))] 14 | public void Delete(DatabaseDriver database) 15 | { 16 | using var con = database.GetConnection(); 17 | var id = Convert.ToInt32(con.Insert(new Product { Name = "blah" })); 18 | var product = con.Get(id); 19 | Assert.NotNull(product); 20 | Assert.Equal("blah", product!.Name); 21 | Assert.Equal(id, product.ProductId); 22 | 23 | con.Delete(product); 24 | Assert.Null(con.Get(id)); 25 | } 26 | 27 | [Theory] 28 | [ClassData(typeof(DatabaseTestData))] 29 | public async Task DeleteAsync(DatabaseDriver database) 30 | { 31 | using var con = database.GetConnection(); 32 | var id = Convert.ToInt32(await con.InsertAsync(new Product { Name = "blah" })); 33 | var product = await con.GetAsync(id); 34 | Assert.NotNull(product); 35 | Assert.Equal("blah", product!.Name); 36 | Assert.Equal(id, product.ProductId); 37 | 38 | await con.DeleteAsync(product); 39 | Assert.Null(await con.GetAsync(id)); 40 | } 41 | 42 | [Theory] 43 | [ClassData(typeof(DatabaseTestData))] 44 | public void DeleteAll(DatabaseDriver database) 45 | { 46 | using var con = database.GetConnection(); 47 | Assert.True(con.DeleteAll() > 0); 48 | Assert.Empty(con.GetAll()); 49 | } 50 | 51 | [Theory] 52 | [ClassData(typeof(DatabaseTestData))] 53 | public async Task DeleteAllAsync(DatabaseDriver database) 54 | { 55 | using var con = database.GetConnection(); 56 | Assert.True(await con.DeleteAllAsync() > 0); 57 | Assert.Empty(await con.GetAllAsync()); 58 | } 59 | 60 | [Theory] 61 | [ClassData(typeof(DatabaseTestData))] 62 | public void DeleteMultiple(DatabaseDriver database) 63 | { 64 | using var con = database.GetConnection(); 65 | var ps = new List 66 | { 67 | new Product { Name = "blah"}, 68 | new Product { Name = "blah"}, 69 | new Product { Name = "blah"}, 70 | }; 71 | 72 | con.InsertAll(ps); 73 | 74 | Assert.Equal(3, con.Select(p => p.Name == "blah").Count()); 75 | 76 | con.DeleteMultiple(p => p.Name == "blah"); 77 | } 78 | 79 | [Theory] 80 | [ClassData(typeof(DatabaseTestData))] 81 | public async Task DeleteMultipleAsync(DatabaseDriver database) 82 | { 83 | using var con = database.GetConnection(); 84 | var ps = new List 85 | { 86 | new Product { Name = "blah"}, 87 | new Product { Name = "blah"}, 88 | new Product { Name = "blah"}, 89 | }; 90 | 91 | await con.InsertAllAsync(ps); 92 | 93 | Assert.Equal(3, (await con.SelectAsync(p => p.Name == "blah")).Count()); 94 | 95 | await con.DeleteMultipleAsync(p => p.Name == "blah"); 96 | } 97 | 98 | [Theory] 99 | [ClassData(typeof(DatabaseTestData))] 100 | public void DeleteMultipleLike(DatabaseDriver database) 101 | { 102 | using var con = database.GetConnection(); 103 | var ps = new List 104 | { 105 | new Product { Name = "blah"}, 106 | new Product { Name = "blah"}, 107 | new Product { Name = "blah"}, 108 | }; 109 | 110 | con.InsertAll(ps); 111 | 112 | Assert.Equal(3, con.Select(p => p.Name == "blah").Count()); 113 | 114 | con.DeleteMultiple(p => p.Name!.Contains("bla")); 115 | } 116 | 117 | [Theory] 118 | [ClassData(typeof(DatabaseTestData))] 119 | public async Task DeleteMultipleAsyncLike(DatabaseDriver database) 120 | { 121 | using var con = database.GetConnection(); 122 | var ps = new List 123 | { 124 | new Product { Name = "blah"}, 125 | new Product { Name = "blah"}, 126 | new Product { Name = "blah"}, 127 | }; 128 | 129 | await con.InsertAllAsync(ps); 130 | 131 | Assert.Equal(3, (await con.SelectAsync(p => p.Name == "blah")).Count()); 132 | 133 | await con.DeleteMultipleAsync(p => p.Name!.Contains("bla")); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Dommel.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | false 5 | 6 | 7 | 8 | 9 | runtime; build; native; contentfiles; analyzers 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/FirstOrDefaultTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class FirstOrDefaultTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void FirstOrDefault_Equals(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | var product = con.FirstOrDefault(p => p.CategoryId == 1); 15 | Assert.NotNull(product); 16 | } 17 | [Theory] 18 | [ClassData(typeof(DatabaseTestData))] 19 | public async Task FirstOrDefaultAsync_Equals(DatabaseDriver database) 20 | { 21 | using var con = database.GetConnection(); 22 | var product = await con.FirstOrDefaultAsync(p => p.CategoryId == 1); 23 | Assert.NotNull(product); 24 | } 25 | 26 | [Theory] 27 | [ClassData(typeof(DatabaseTestData))] 28 | public void FirstOrDefault_ContainsConstant(DatabaseDriver database) 29 | { 30 | using var con = database.GetConnection(); 31 | var product = con.FirstOrDefault(p => p.Name!.Contains("Anton")); 32 | Assert.NotNull(product); 33 | } 34 | 35 | [Theory] 36 | [ClassData(typeof(DatabaseTestData))] 37 | public async Task FirstOrDefaultAsync_ContainsConstant(DatabaseDriver database) 38 | { 39 | using var con = database.GetConnection(); 40 | var product = await con.FirstOrDefaultAsync(p => p.Name!.Contains("Anton")); 41 | Assert.NotNull(product); 42 | } 43 | 44 | [Theory] 45 | [ClassData(typeof(DatabaseTestData))] 46 | public void FirstOrDefault_ContainsVariable(DatabaseDriver database) 47 | { 48 | var productName = "Anton"; 49 | using var con = database.GetConnection(); 50 | var product = con.FirstOrDefault(p => p.Name!.Contains(productName)); 51 | Assert.NotNull(product); 52 | } 53 | 54 | [Theory] 55 | [ClassData(typeof(DatabaseTestData))] 56 | public async Task FirstOrDefaultAsync_ContainsVariable(DatabaseDriver database) 57 | { 58 | var productName = "Anton"; 59 | using var con = database.GetConnection(); 60 | var product = await con.FirstOrDefaultAsync(p => p.Name!.Contains(productName)); 61 | Assert.NotNull(product); 62 | } 63 | 64 | [Theory] 65 | [ClassData(typeof(DatabaseTestData))] 66 | public void FirstOrDefault_StartsWith(DatabaseDriver database) 67 | { 68 | var productName = "Cha"; 69 | using var con = database.GetConnection(); 70 | var product = con.FirstOrDefault(p => p.Name!.StartsWith(productName)); 71 | Assert.NotNull(product); 72 | } 73 | 74 | [Theory] 75 | [ClassData(typeof(DatabaseTestData))] 76 | public async Task FirstOrDefaultAsync_StartsWith(DatabaseDriver database) 77 | { 78 | var productName = "Cha"; 79 | using var con = database.GetConnection(); 80 | var product = await con.FirstOrDefaultAsync(p => p.Name!.StartsWith(productName)); 81 | Assert.NotNull(product); 82 | } 83 | 84 | [Theory] 85 | [ClassData(typeof(DatabaseTestData))] 86 | public void FirstOrDefault_EndsWith(DatabaseDriver database) 87 | { 88 | var productName = "2"; 89 | using var con = database.GetConnection(); 90 | var product = con.FirstOrDefault(p => p.Name!.EndsWith(productName)); 91 | Assert.NotNull(product); 92 | } 93 | 94 | [Theory] 95 | [ClassData(typeof(DatabaseTestData))] 96 | public async Task FirstOrDefaultAsync_EndsWith(DatabaseDriver database) 97 | { 98 | var productName = "2"; 99 | using var con = database.GetConnection(); 100 | var product = await con.FirstOrDefaultAsync(p => p.Name!.EndsWith(productName)); 101 | Assert.NotNull(product); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/FromTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace Dommel.IntegrationTests; 6 | 7 | [Collection("Database")] 8 | public class FromTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(DatabaseTestData))] 12 | public void SelectAllSync(DatabaseDriver database) 13 | { 14 | using var con = database.GetConnection(); 15 | var products = con.From(sql => sql.Select()); 16 | Assert.NotEmpty(products); 17 | } 18 | 19 | [Theory] 20 | [ClassData(typeof(DatabaseTestData))] 21 | public void SelectProjectSync(DatabaseDriver database) 22 | { 23 | using var con = database.GetConnection(); 24 | var products = con.From(sql => 25 | sql.Select(p => new { p.ProductId, p.Name })); 26 | Assert.NotEmpty(products); 27 | Assert.All(products, p => Assert.Equal(0, p.CategoryId)); 28 | } 29 | 30 | [Theory] 31 | [ClassData(typeof(DatabaseTestData))] 32 | public async Task SelectAll(DatabaseDriver database) 33 | { 34 | using var con = database.GetConnection(); 35 | var products = await con.FromAsync(sql => sql.Select()); 36 | Assert.NotEmpty(products); 37 | } 38 | 39 | [Theory] 40 | [ClassData(typeof(DatabaseTestData))] 41 | public async Task SelectProject(DatabaseDriver database) 42 | { 43 | using var con = database.GetConnection(); 44 | var products = await con.FromAsync(sql => 45 | sql.Select(p => new { p.ProductId, p.Name })); 46 | Assert.NotEmpty(products); 47 | Assert.All(products, p => Assert.Equal(0, p.CategoryId)); 48 | } 49 | 50 | [Theory] 51 | [ClassData(typeof(DatabaseTestData))] 52 | public async Task Select_Where(DatabaseDriver database) 53 | { 54 | using var con = database.GetConnection(); 55 | var products = await con.FromAsync( 56 | sql => sql.Select(p => new { p.Name, p.CategoryId }) 57 | .Where(p => p.Name!.StartsWith("Chai"))); 58 | 59 | Assert.NotEmpty(products); 60 | Assert.All(products, p => Assert.StartsWith("Chai", p.Name)); 61 | } 62 | 63 | [Theory] 64 | [ClassData(typeof(DatabaseTestData))] 65 | public async Task OrderBy(DatabaseDriver database) 66 | { 67 | using var con = database.GetConnection(); 68 | var products = await con.FromAsync( 69 | sql => sql.OrderBy(p => p.Name).Select()); 70 | 71 | Assert.NotEmpty(products); 72 | } 73 | 74 | [Theory] 75 | [ClassData(typeof(DatabaseTestData))] 76 | public async Task OrderByPropertyInfo(DatabaseDriver database) 77 | { 78 | using var con = database.GetConnection(); 79 | var products = await con.FromAsync( 80 | sql => sql.OrderBy(typeof(Product).GetProperty("Name")!).Select()); 81 | 82 | Assert.NotEmpty(products); 83 | } 84 | 85 | [Theory] 86 | [ClassData(typeof(DatabaseTestData))] 87 | public async Task KitchenSink(DatabaseDriver database) 88 | { 89 | using var con = database.GetConnection(); 90 | var products = await con.FromAsync(sql => 91 | sql.Select(p => new { p.Name, p.CategoryId }) 92 | .Where(p => p.Name!.StartsWith("Chai") && p.CategoryId == 1) 93 | .OrWhere(p => p.Name != null) 94 | .AndWhere(p => p.CategoryId != 0) 95 | .OrderBy(p => p.CategoryId) 96 | .OrderByDescending(p => p.Name) 97 | .Page(1, 5)); 98 | 99 | Assert.Equal(5, products.Count()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/GetAllTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class GetAllTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void GetAll(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | var products = con.GetAll(); 15 | Assert.NotEmpty(products); 16 | Assert.All(products, p => Assert.NotEmpty(p.Name!)); 17 | } 18 | 19 | [Theory] 20 | [ClassData(typeof(DatabaseTestData))] 21 | public async Task GetAllAsync(DatabaseDriver database) 22 | { 23 | using var con = database.GetConnection(); 24 | var products = await con.GetAllAsync(); 25 | Assert.NotEmpty(products); 26 | Assert.All(products, p => Assert.NotEmpty(p.Name!)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/GetTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Dommel.IntegrationTests; 8 | 9 | [Collection("Database")] 10 | public class GetTests 11 | { 12 | [Theory] 13 | [ClassData(typeof(DatabaseTestData))] 14 | public void Get(DatabaseDriver database) 15 | { 16 | using var con = database.GetConnection(); 17 | var product = con.Get(1); 18 | Assert.NotNull(product); 19 | Assert.NotEmpty(product!.Name!); 20 | } 21 | 22 | [Theory] 23 | [ClassData(typeof(DatabaseTestData))] 24 | public async Task GetAsync(DatabaseDriver database) 25 | { 26 | using var con = database.GetConnection(); 27 | var product = await con.GetAsync(1); 28 | Assert.NotNull(product); 29 | Assert.NotEmpty(product!.Name!); 30 | } 31 | 32 | [Theory] 33 | [ClassData(typeof(DatabaseTestData))] 34 | public void Get_ParamsOverload(DatabaseDriver database) 35 | { 36 | using var con = database.GetConnection(); 37 | var product = con.Get(new object[] { 1 }); 38 | Assert.NotNull(product); 39 | Assert.NotEmpty(product!.Name!); 40 | } 41 | 42 | [Theory] 43 | [ClassData(typeof(DatabaseTestData))] 44 | public async Task GetAsync_ParamsOverload(DatabaseDriver database) 45 | { 46 | using var con = database.GetConnection(); 47 | var product = await con.GetAsync(new object[] { 1 }); 48 | Assert.NotNull(product); 49 | Assert.NotEmpty(product!.Name!); 50 | } 51 | 52 | [Theory] 53 | [ClassData(typeof(DatabaseTestData))] 54 | public void Get_ThrowsWhenCompositeKey(DatabaseDriver database) 55 | { 56 | using var con = database.GetConnection(); 57 | var ex = Assert.Throws(() => con.Get(1)); 58 | Assert.Equal("Entity ProductsCategories contains more than one key property.Use the Get overload which supports passing multiple IDs.", ex.Message); 59 | } 60 | 61 | [Theory] 62 | [ClassData(typeof(DatabaseTestData))] 63 | public async Task GetAsync_ThrowsWhenCompositeKey(DatabaseDriver database) 64 | { 65 | using var con = database.GetConnection(); 66 | var ex = await Assert.ThrowsAsync(() => con.GetAsync(1)); 67 | Assert.Equal("Entity ProductsCategories contains more than one key property.Use the Get overload which supports passing multiple IDs.", ex.Message); 68 | } 69 | 70 | [Theory] 71 | [ClassData(typeof(DatabaseTestData))] 72 | public void Get_CompositeKey(DatabaseDriver database) 73 | { 74 | using var con = database.GetConnection(); 75 | var product = con.Get(1, 1); 76 | Assert.NotNull(product); 77 | Assert.Equal(1, product!.ProductId); 78 | Assert.Equal(1, product.CategoryId); 79 | } 80 | 81 | [Theory] 82 | [ClassData(typeof(DatabaseTestData))] 83 | public async Task GetAsync_CompositeKey(DatabaseDriver database) 84 | { 85 | using var con = database.GetConnection(); 86 | var product = await con.GetAsync(1, 1); 87 | Assert.NotNull(product); 88 | Assert.Equal(1, product!.ProductId); 89 | Assert.Equal(1, product.CategoryId); 90 | } 91 | 92 | [Theory] 93 | [ClassData(typeof(DatabaseTestData))] 94 | public void Get_ThrowsWhenCompositeKeyArgumentsDontMatch(DatabaseDriver database) 95 | { 96 | DommelMapper.QueryCache.Clear(); 97 | using var con = database.GetConnection(); 98 | var ex = Assert.Throws(() => con.Get(1, 2, 3)); 99 | Assert.Equal("Number of key columns (2) of type ProductsCategories does not match with the number of specified IDs (3).", ex.Message); 100 | } 101 | 102 | [Theory] 103 | [ClassData(typeof(DatabaseTestData))] 104 | public async Task GetAsync_ThrowsWhenCompositeKeyArgumentsDontMatch(DatabaseDriver database) 105 | { 106 | DommelMapper.QueryCache.Clear(); 107 | using var con = database.GetConnection(); 108 | var ex = await Assert.ThrowsAsync(() => con.GetAsync(1, 2, 3)); 109 | Assert.Equal("Number of key columns (2) of type ProductsCategories does not match with the number of specified IDs (3).", ex.Message); 110 | } 111 | } 112 | 113 | public class ProductsCategories 114 | { 115 | [Key] 116 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 117 | public int ProductId { get; set; } 118 | 119 | [Key] 120 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 121 | public int CategoryId { get; set; } 122 | } -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Infrastructure/CI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel.IntegrationTests; 4 | 5 | public static class CI 6 | { 7 | public static bool IsAppVeyor => EnvBool("APPVEYOR"); 8 | 9 | public static bool IsTravis => EnvBool("TRAVIS"); 10 | 11 | private static bool EnvBool(string env) => bool.TryParse(Environment.GetEnvironmentVariable(env), out var b) && b; 12 | } 13 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Infrastructure/DatabaseCollection.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.IntegrationTests; 4 | 5 | // Apply the text fixture to all tests in the "Database" collection 6 | [CollectionDefinition("Database")] 7 | public class DatabaseCollection : ICollectionFixture 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Infrastructure/DatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Dommel.IntegrationTests; 7 | 8 | public class DatabaseFixture : DatabaseFixtureBase 9 | { 10 | protected override TheoryData Drivers => new DatabaseTestData(); 11 | } 12 | 13 | public abstract class DatabaseFixtureBase : IAsyncLifetime 14 | { 15 | private readonly DatabaseDriver[] _databases; 16 | 17 | public DatabaseFixtureBase() 18 | { 19 | // Extract the database drivers from the test data 20 | _databases = Drivers 21 | .Cast() 22 | .ToArray(); 23 | 24 | if (_databases.Length == 0) 25 | { 26 | throw new InvalidOperationException($"No databases defined in {nameof(DatabaseTestData)} theory data."); 27 | } 28 | } 29 | 30 | protected abstract TheoryData Drivers { get; } 31 | 32 | public async Task InitializeAsync() 33 | { 34 | foreach (var database in _databases) 35 | { 36 | await database.InitializeAsync(); 37 | } 38 | } 39 | 40 | public async Task DisposeAsync() 41 | { 42 | foreach (var database in _databases) 43 | { 44 | await database.DisposeAsync(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Infrastructure/DatabaseTestData.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.IntegrationTests; 4 | 5 | public class DatabaseTestData : TheoryData 6 | { 7 | public DatabaseTestData() 8 | { 9 | // Defines the database providers to use for each test method. 10 | // These providers are used to initialize the databases in the 11 | // DatabaseFixture as well. 12 | if (!CI.IsTravis) 13 | { 14 | Add(new SqlServerDatabaseDriver()); 15 | } 16 | 17 | Add(new MySqlDatabaseDriver()); 18 | Add(new PostgresDatabaseDriver()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/InsertNonGeneratedColumnTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace Dommel.IntegrationTests; 6 | 7 | [Collection("Database")] 8 | public class InsertNonGeneratedColumnTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(DatabaseTestData))] 12 | public async Task InsertAsync(DatabaseDriver database) 13 | { 14 | using var con = database.GetConnection(); 15 | // Arrange 16 | var generatedId = Guid.NewGuid(); 17 | 18 | // Act 19 | _ = await con.InsertAsync(new Baz { BazId = generatedId }); 20 | 21 | // Assert 22 | var product = await con.GetAsync(generatedId); 23 | Assert.NotNull(product); 24 | Assert.Equal(generatedId, product!.BazId); 25 | Assert.Equal("Baz", product.Name); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/InsertOutputParameterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Dapper; 4 | using Microsoft.Data.SqlClient; 5 | using Xunit; 6 | 7 | namespace Dommel.IntegrationTests; 8 | 9 | [Collection("Database")] 10 | public class InsertOutputParameterTests 11 | { 12 | public class Qux 13 | { 14 | public Guid Id { get; set; } = Guid.NewGuid(); 15 | 16 | public string? Name { get; set; } 17 | } 18 | 19 | public class GuidSqlServerSqlBuilder : SqlServerSqlBuilder 20 | { 21 | public override string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) => 22 | $"set nocount on insert into {tableName} ({string.Join(", ", columnNames)}) output inserted.Id values ({string.Join(", ", paramNames)})"; 23 | } 24 | 25 | [Fact] 26 | public async Task InsertGuidPrimaryKey() 27 | { 28 | if (CI.IsTravis) 29 | { 30 | // Don't run SQL Server test on Linux 31 | return; 32 | } 33 | 34 | using var con = new SqlServerDatabaseDriver().GetConnection(); 35 | await con.ExecuteAsync("CREATE TABLE dbo.Quxs (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWID(), Name VARCHAR(255));"); 36 | try 37 | { 38 | object identity; 39 | try 40 | { 41 | DommelMapper.AddSqlBuilder(typeof(SqlConnection), new GuidSqlServerSqlBuilder()); 42 | identity = await con.InsertAsync(new Qux { Name = "blah" }); 43 | } 44 | finally 45 | { 46 | DommelMapper.AddSqlBuilder(typeof(SqlConnection), new SqlServerSqlBuilder()); 47 | } 48 | 49 | Assert.NotNull(identity); 50 | var id = Assert.IsType(identity); 51 | var baz = await con.GetAsync(id); 52 | Assert.NotNull(baz); 53 | Assert.Equal("blah", baz!.Name); 54 | Assert.Equal(id, baz.Id); 55 | } 56 | finally 57 | { 58 | await con.ExecuteAsync("DROP TABLE dbo.Quxs"); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/InsertTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Dommel.IntegrationTests; 8 | 9 | [Collection("Database")] 10 | public class InsertTests 11 | { 12 | [Theory] 13 | [ClassData(typeof(DatabaseTestData))] 14 | public void Insert(DatabaseDriver database) 15 | { 16 | // Arrange 17 | using var con = database.GetConnection(); 18 | var productToInsert = new Product { Name = "Foo Product" }; 19 | productToInsert.SetSlug("foo-product"); 20 | 21 | // Act 22 | var id = Convert.ToInt32(con.Insert(productToInsert)); 23 | 24 | // Assert 25 | var product = con.Get(id); 26 | Assert.NotNull(product); 27 | Assert.Equal(id, product!.ProductId); 28 | Assert.Equal("Foo Product", product.Name); 29 | Assert.Equal("foo-product", product.Slug); 30 | } 31 | 32 | [Theory] 33 | [ClassData(typeof(DatabaseTestData))] 34 | public async Task InsertAsync(DatabaseDriver database) 35 | { 36 | // Arrange 37 | using var con = database.GetConnection(); 38 | var productToInsert = new Product { Name = "Foo Product" }; 39 | productToInsert.SetSlug("foo-product"); 40 | 41 | // Act 42 | var id = Convert.ToInt32(await con.InsertAsync(productToInsert)); 43 | 44 | // Assert 45 | var product = await con.GetAsync(id); 46 | Assert.NotNull(product); 47 | Assert.Equal(id, product!.ProductId); 48 | Assert.Equal("Foo Product", product.Name); 49 | Assert.Equal("foo-product", product.Slug); 50 | } 51 | 52 | [Theory] 53 | [ClassData(typeof(DatabaseTestData))] 54 | public void InsertAll(DatabaseDriver database) 55 | { 56 | using var con = database.GetConnection(); 57 | var ps = new List 58 | { 59 | new Foo { Name = "blah" }, 60 | new Foo { Name = "blah" }, 61 | new Foo { Name = "blah" }, 62 | }; 63 | 64 | con.InsertAll(ps); 65 | 66 | var blahs = con.Select(p => p.Name == "blah"); 67 | Assert.Equal(3, blahs.Count()); 68 | } 69 | 70 | [Theory] 71 | [ClassData(typeof(DatabaseTestData))] 72 | public async Task InsertAllAsync(DatabaseDriver database) 73 | { 74 | using var con = database.GetConnection(); 75 | var ps = new List 76 | { 77 | new Bar { Name = "blah" }, 78 | new Bar { Name = "blah" }, 79 | new Bar { Name = "blah" }, 80 | }; 81 | 82 | await con.InsertAllAsync(ps); 83 | 84 | var blahs = await con.SelectAsync(p => p.Name == "blah"); 85 | Assert.Equal(3, blahs.Count()); 86 | } 87 | 88 | [Theory] 89 | [ClassData(typeof(DatabaseTestData))] 90 | public void InsertAllEmtyList(DatabaseDriver database) 91 | { 92 | using var con = database.GetConnection(); 93 | var ps = new List(); 94 | con.InsertAll(ps); 95 | } 96 | 97 | [Theory] 98 | [ClassData(typeof(DatabaseTestData))] 99 | public async Task InsertAllAsyncEmtyList(DatabaseDriver database) 100 | { 101 | using var con = database.GetConnection(); 102 | var ps = new List(); 103 | await con.InsertAllAsync(ps); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/Models.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace Dommel.IntegrationTests; 7 | 8 | public abstract class FullNamedEntity 9 | { 10 | [Column("FullName")] 11 | public string? Name { get; set; } 12 | } 13 | 14 | public abstract class NamedEntity 15 | { 16 | public string? Name { get; set; } 17 | } 18 | 19 | public class Product : FullNamedEntity 20 | { 21 | [Key] 22 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 23 | public int ProductId { get; set; } 24 | 25 | public string? Slug { get; private set; } 26 | 27 | public void SetSlug(string slug) => Slug = slug; 28 | 29 | // The foreign key to Categories table 30 | public int CategoryId { get; set; } 31 | 32 | // The navigation property 33 | public Category? Category { get; set; } 34 | 35 | // One Product has many Options 36 | public ICollection Options { get; set; } = new List(); 37 | } 38 | 39 | public class Category : NamedEntity, IEquatable 40 | { 41 | [Key] 42 | public int CategoryId { get; set; } 43 | 44 | public bool Equals(Category? other) => CategoryId == other?.CategoryId; 45 | } 46 | 47 | public class ProductOption : IEquatable 48 | { 49 | public int Id { get; set; } 50 | 51 | // One ProductOption has one Product (no navigation) 52 | // Represents the foreign key to the product table 53 | public int ProductId { get; set; } 54 | 55 | public bool Equals(ProductOption? other) => Id == other?.Id; 56 | } 57 | 58 | public class Order 59 | { 60 | public int Id { get; set; } 61 | 62 | public DateTime Created { get; set; } = DateTime.UtcNow; 63 | 64 | [ForeignKey(nameof(OrderLine.OrderId))] 65 | public ICollection? OrderLines { get; set; } 66 | } 67 | 68 | public class OrderLine 69 | { 70 | public int Id { get; set; } 71 | 72 | public int OrderId { get; set; } 73 | 74 | public string? Line { get; set; } 75 | } 76 | 77 | public class Foo : NamedEntity 78 | { 79 | public Foo() 80 | { 81 | Name = nameof(Foo); 82 | } 83 | 84 | public int Id { get; set; } 85 | } 86 | 87 | public class Bar : NamedEntity 88 | { 89 | public Bar() 90 | { 91 | Name = nameof(Bar); 92 | } 93 | 94 | public int Id { get; set; } 95 | } 96 | 97 | public class Baz 98 | { 99 | [Key] 100 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 101 | public Guid BazId { get; set; } 102 | 103 | public string? Name { get; set; } = nameof(Baz); 104 | } -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/MultiMapTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class MultiMapTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void Get(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | var product = con.Get(1, (p, c) => 15 | { 16 | p.Category = c; 17 | return p; 18 | }); 19 | 20 | Assert.NotNull(product); 21 | Assert.NotEmpty(product!.Name!); 22 | Assert.NotNull(product.Category); 23 | Assert.NotNull(product.Category?.Name); 24 | } 25 | 26 | [Theory] 27 | [ClassData(typeof(DatabaseTestData))] 28 | public async Task GetAsync(DatabaseDriver database) 29 | { 30 | using var con = database.GetConnection(); 31 | var product = await con.GetAsync(1, (p, c) => 32 | { 33 | p.Category = c; 34 | return p; 35 | }); 36 | 37 | Assert.NotNull(product); 38 | Assert.NotEmpty(product!.Name!); 39 | Assert.NotNull(product.Category); 40 | Assert.NotNull(product.Category?.Name); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/PagingTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace Dommel.IntegrationTests; 6 | 7 | [Collection("Database")] 8 | public class PagingTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(DatabaseTestData))] 12 | public void GetPaged_FetchesFirstPage(DatabaseDriver database) 13 | { 14 | using var con = database.GetConnection(); 15 | var paged = con.GetPaged(1, 5); 16 | Assert.Equal(5, paged.Count()); 17 | Assert.Collection(paged, 18 | p => Assert.Equal("Chai", p.Name), 19 | p => Assert.Equal("Chang", p.Name), 20 | p => Assert.Equal("Aniseed Syrup", p.Name), 21 | p => Assert.Equal("Chef Anton's Cajun Seasoning", p.Name), 22 | p => Assert.Equal("Chef Anton's Gumbo Mix", p.Name)); 23 | } 24 | 25 | [Theory] 26 | [ClassData(typeof(DatabaseTestData))] 27 | public async Task GetPagedAsync_FetchesFirstPage(DatabaseDriver database) 28 | { 29 | using var con = database.GetConnection(); 30 | var paged = await con.GetPagedAsync(1, 5); 31 | Assert.Equal(5, paged.Count()); 32 | Assert.Collection(paged, 33 | p => Assert.Equal("Chai", p.Name), 34 | p => Assert.Equal("Chang", p.Name), 35 | p => Assert.Equal("Aniseed Syrup", p.Name), 36 | p => Assert.Equal("Chef Anton's Cajun Seasoning", p.Name), 37 | p => Assert.Equal("Chef Anton's Gumbo Mix", p.Name)); 38 | } 39 | 40 | [Theory] 41 | [ClassData(typeof(DatabaseTestData))] 42 | public void GetPaged_FetchesSecondPage(DatabaseDriver database) 43 | { 44 | using var con = database.GetConnection(); 45 | var paged = con.GetPaged(2, 5); 46 | Assert.Equal(5, paged.Count()); 47 | } 48 | 49 | [Theory] 50 | [ClassData(typeof(DatabaseTestData))] 51 | public async Task GetPagedAsync_FetchesSecondPage(DatabaseDriver database) 52 | { 53 | using var con = database.GetConnection(); 54 | var paged = await con.GetPagedAsync(2, 5); 55 | Assert.Equal(5, paged.Count()); 56 | } 57 | 58 | [Theory] 59 | [ClassData(typeof(DatabaseTestData))] 60 | public void GetPaged_FetchesThirdPartialPage(DatabaseDriver database) 61 | { 62 | using var con = database.GetConnection(); 63 | var paged = con.GetPaged(3, 5); 64 | Assert.True(paged.Count() >= 3, "Should contain at least 3 items"); 65 | } 66 | 67 | [Theory] 68 | [ClassData(typeof(DatabaseTestData))] 69 | public async Task GetPagedAsync_FetchesThirdPartialPage(DatabaseDriver database) 70 | { 71 | using var con = database.GetConnection(); 72 | var paged = await con.GetPagedAsync(3, 5); 73 | Assert.True(paged.Count() >= 3, "Should contain at least 3 items"); 74 | } 75 | 76 | [Theory] 77 | [ClassData(typeof(DatabaseTestData))] 78 | public void SelectPaged_FetchesFirstPage(DatabaseDriver database) 79 | { 80 | using var con = database.GetConnection(); 81 | var paged = con.SelectPaged(p => p.Name == "Chai", 1, 5); 82 | Assert.Single(paged); 83 | } 84 | 85 | [Theory] 86 | [ClassData(typeof(DatabaseTestData))] 87 | public async Task SelectPagedAsync_FetchesFirstPage(DatabaseDriver database) 88 | { 89 | using var con = database.GetConnection(); 90 | var paged = await con.SelectPagedAsync(p => p.Name == "Chai", 1, 5); 91 | Assert.Single(paged); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/ProjectTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Dommel.IntegrationTests; 7 | 8 | [Collection("Database")] 9 | public class ProjectTests 10 | { 11 | [Theory] 12 | [ClassData(typeof(DatabaseTestData))] 13 | public void Project(DatabaseDriver database) 14 | { 15 | using var con = database.GetConnection(); 16 | var p = con.Project(1); 17 | Assert.NotNull(p); 18 | Assert.NotEqual(0, p!.ProductId); 19 | Assert.NotNull(p.Name); 20 | } 21 | 22 | [Theory] 23 | [ClassData(typeof(DatabaseTestData))] 24 | public async Task ProjectAsync(DatabaseDriver database) 25 | { 26 | using var con = database.GetConnection(); 27 | var p = await con.ProjectAsync(1); 28 | Assert.NotNull(p); 29 | Assert.NotEqual(0, p!.ProductId); 30 | Assert.NotNull(p.Name); 31 | } 32 | 33 | [Theory] 34 | [ClassData(typeof(DatabaseTestData))] 35 | public void ProjectAll(DatabaseDriver database) 36 | { 37 | using var con = database.GetConnection(); 38 | var ps = con.ProjectAll(); 39 | Assert.NotEmpty(ps); 40 | Assert.All(ps, p => 41 | { 42 | Assert.NotEqual(0, p.ProductId); 43 | Assert.NotNull(p.Name); 44 | }); 45 | } 46 | 47 | [Theory] 48 | [ClassData(typeof(DatabaseTestData))] 49 | public async Task ProjectAllAsync(DatabaseDriver database) 50 | { 51 | using var con = database.GetConnection(); 52 | var ps = await con.ProjectAllAsync(); 53 | Assert.NotEmpty(ps); 54 | Assert.All(ps, p => 55 | { 56 | Assert.NotEqual(0, p.ProductId); 57 | Assert.NotNull(p.Name); 58 | }); 59 | } 60 | [Theory] 61 | [ClassData(typeof(DatabaseTestData))] 62 | public void ProjectPagedAll(DatabaseDriver database) 63 | { 64 | using var con = database.GetConnection(); 65 | var ps = con.ProjectPaged(pageNumber: 1, pageSize: 5); 66 | Assert.NotEmpty(ps); 67 | Assert.All(ps, p => 68 | { 69 | Assert.NotEqual(0, p.ProductId); 70 | Assert.NotNull(p.Name); 71 | }); 72 | } 73 | 74 | [Theory] 75 | [ClassData(typeof(DatabaseTestData))] 76 | public async Task ProjectPagedAsync(DatabaseDriver database) 77 | { 78 | using var con = database.GetConnection(); 79 | var ps = await con.ProjectPagedAsync(pageNumber: 1, pageSize: 5); 80 | Assert.NotEmpty(ps); 81 | Assert.All(ps, p => 82 | { 83 | Assert.NotEqual(0, p.ProductId); 84 | Assert.NotNull(p.Name); 85 | }); 86 | } 87 | 88 | // Subset of the default Product entity 89 | [Table("Products")] 90 | public class ProductSmall 91 | { 92 | [Key] 93 | public int ProductId { get; set; } 94 | 95 | [Column("FullName")] 96 | public string? Name { get; set; } 97 | } 98 | } -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/ResolversTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Xunit; 4 | 5 | namespace Dommel.Tests; 6 | 7 | public class ResolversTests 8 | { 9 | private readonly SqlConnection _sqlConnection = new(); 10 | 11 | [Fact] 12 | public void Table_WithSchema() 13 | { 14 | Assert.Equal("[dbo].[Qux]", Resolvers.Table(typeof(FooQux), _sqlConnection)); 15 | Assert.Equal("[foo].[dbo].[Qux]", Resolvers.Table(typeof(FooDboQux), _sqlConnection)); 16 | } 17 | 18 | [Fact] 19 | public void Table_NoCacheConflictNestedClass() 20 | { 21 | Assert.Equal("[BarA]", Resolvers.Table(typeof(Foo.Bar), _sqlConnection)); 22 | Assert.Equal("[BarB]", Resolvers.Table(typeof(Baz.Bar), _sqlConnection)); 23 | } 24 | 25 | public class Foo 26 | { 27 | [Table("BarA")] 28 | public class Bar 29 | { 30 | [Column("BazA")] 31 | public string? Baz { get; set; } 32 | } 33 | 34 | [Table("BarA")] 35 | public class BarChild 36 | { 37 | public int BarId { get; set; } 38 | } 39 | } 40 | 41 | public class Baz 42 | { 43 | [Table("BarB")] 44 | public class Bar 45 | { 46 | [Column("BazB")] 47 | public string? Baz { get; set; } 48 | } 49 | 50 | [Table("BarA")] 51 | public class BarChild 52 | { 53 | public int BarId { get; set; } 54 | } 55 | } 56 | 57 | [Table("Qux", Schema = "foo.dbo")] 58 | public class FooDboQux 59 | { 60 | public int Id { get; set; } 61 | } 62 | 63 | [Table("Qux", Schema = "dbo")] 64 | public class FooQux 65 | { 66 | public int Id { get; set; } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/SelectAutoMultiMapTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace Dommel.IntegrationTests; 6 | 7 | [Collection("Database")] 8 | public class SelectAutoMultiMapTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(DatabaseTestData))] 12 | public void SelectMultiMap(DatabaseDriver database) 13 | { 14 | using var con = database.GetConnection(); 15 | var products = con.Select(x => x.Name != null && x.CategoryId == 1); 16 | Assert.All(products, x => Assert.NotNull(x.Category)); 17 | var first = products.First(); 18 | Assert.NotEmpty(first.Options); 19 | } 20 | 21 | [Theory] 22 | [ClassData(typeof(DatabaseTestData))] 23 | public void FirstOrDefaultMultiMap(DatabaseDriver database) 24 | { 25 | using var con = database.GetConnection(); 26 | var product = con.FirstOrDefault(x => x.Name != null && x.CategoryId == 1); 27 | Assert.NotNull(product); 28 | Assert.NotNull(product!.Category); 29 | Assert.NotEmpty(product.Options); 30 | } 31 | 32 | [Theory] 33 | [ClassData(typeof(DatabaseTestData))] 34 | public async Task SelectAsyncMultiMap(DatabaseDriver database) 35 | { 36 | using var con = database.GetConnection(); 37 | var products = await con.SelectAsync(x => x.Name != null && x.CategoryId == 1); 38 | Assert.All(products, x => Assert.NotNull(x.Category)); 39 | var first = products.First(); 40 | Assert.NotEmpty(first.Options); 41 | } 42 | 43 | [Theory] 44 | [ClassData(typeof(DatabaseTestData))] 45 | public async Task FirstOrDefaultAsyncMultiMap(DatabaseDriver database) 46 | { 47 | using var con = database.GetConnection(); 48 | var product = await con.FirstOrDefaultAsync(x => x.Name != null && x.CategoryId == 1); 49 | Assert.NotNull(product); 50 | Assert.NotNull(product!.Category); 51 | Assert.NotEmpty(product.Options); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/SelectMultiMapTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Xunit; 4 | 5 | namespace Dommel.IntegrationTests; 6 | 7 | [Collection("Database")] 8 | public class SelectMultiMapTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(DatabaseTestData))] 12 | public void SelectMultiMap(DatabaseDriver database) 13 | { 14 | using var con = database.GetConnection(); 15 | var products = con.Select( 16 | p => p.CategoryId == 1 && p.Name != null, 17 | (p, c, po) => 18 | { 19 | p.Category = c; 20 | if (!p.Options.Contains(po)) 21 | { 22 | p.Options.Add(po); 23 | } 24 | return p; 25 | }); 26 | Assert.Equal(5, products.Count()); 27 | Assert.All(products, x => Assert.NotNull(x.Category)); 28 | } 29 | 30 | [Theory] 31 | [ClassData(typeof(DatabaseTestData))] 32 | public void FirstOrDefaultMultiMap(DatabaseDriver database) 33 | { 34 | using var con = database.GetConnection(); 35 | var product = con.FirstOrDefault( 36 | p => p.CategoryId == 1 && p.Name != null, 37 | (p, c, po) => 38 | { 39 | p.Category = c; 40 | if (!p.Options.Contains(po)) 41 | { 42 | p.Options.Add(po); 43 | } 44 | return p; 45 | }); 46 | Assert.NotNull(product); 47 | Assert.NotNull(product!.Category); 48 | Assert.NotEmpty(product.Options); 49 | } 50 | 51 | [Theory] 52 | [ClassData(typeof(DatabaseTestData))] 53 | public async Task SelectAsyncMultiMap(DatabaseDriver database) 54 | { 55 | using var con = database.GetConnection(); 56 | var products = await con.SelectAsync( 57 | p => p.CategoryId == 1 && p.Name != null, 58 | (p, c, po) => 59 | { 60 | p.Category = c; 61 | if (!p.Options.Contains(po)) 62 | { 63 | p.Options.Add(po); 64 | } 65 | return p; 66 | }); 67 | Assert.Equal(5, products.Count()); 68 | Assert.All(products, x => Assert.NotNull(x.Category)); 69 | } 70 | 71 | [Theory] 72 | [ClassData(typeof(DatabaseTestData))] 73 | public async Task FirstOrDefaultAsyncMultiMap(DatabaseDriver database) 74 | { 75 | using var con = database.GetConnection(); 76 | var product = await con.FirstOrDefaultAsync( 77 | p => p.CategoryId == 1 && p.Name != null, 78 | (p, c, po) => 79 | { 80 | p.Category = c; 81 | if (!p.Options.Contains(po)) 82 | { 83 | p.Options.Add(po); 84 | } 85 | return p; 86 | }); 87 | Assert.NotNull(product); 88 | Assert.NotNull(product!.Category); 89 | Assert.NotEmpty(product.Options); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/TestSample.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class SampleTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void Sample(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | _ = con.GetAll(); 15 | } 16 | 17 | [Theory] 18 | [ClassData(typeof(DatabaseTestData))] 19 | public async Task SampleAsync(DatabaseDriver database) 20 | { 21 | using var con = database.GetConnection(); 22 | _ = await con.GetAllAsync(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/UpdateTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xunit; 3 | 4 | namespace Dommel.IntegrationTests; 5 | 6 | [Collection("Database")] 7 | public class UpdateTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(DatabaseTestData))] 11 | public void Update(DatabaseDriver database) 12 | { 13 | using var con = database.GetConnection(); 14 | var product = con.Get(1); 15 | Assert.NotNull(product); 16 | product!.Name = "Test"; 17 | product.SetSlug("test"); 18 | con.Update(product); 19 | 20 | var newProduct = con.Get(1); 21 | Assert.Equal("Test", newProduct!.Name); 22 | Assert.Equal("test", newProduct.Slug); 23 | } 24 | 25 | [Theory] 26 | [ClassData(typeof(DatabaseTestData))] 27 | public async Task UpdateAsync(DatabaseDriver database) 28 | { 29 | using var con = database.GetConnection(); 30 | var product = await con.GetAsync(1); 31 | Assert.NotNull(product); 32 | product!.Name = "Test"; 33 | product.SetSlug("test"); 34 | await con.UpdateAsync(product); 35 | 36 | var newProduct = await con.GetAsync(1); 37 | Assert.Equal("Test", newProduct!.Name); 38 | Assert.Equal("test", newProduct.Slug); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Dommel.IntegrationTests/coverage.cmd: -------------------------------------------------------------------------------- 1 | dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Include="[Dommel]*" 2 | reportgenerator "-reports:coverage.opencover.xml" "-targetdir:coveragereport" 3 | start coveragereport\index.htm 4 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Databases/JsonMySqlDatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Dapper; 3 | using Dommel.IntegrationTests; 4 | 5 | namespace Dommel.Json.IntegrationTests; 6 | 7 | public class JsonMySqlDatabaseDriver : MySqlDatabaseDriver 8 | { 9 | public override string DefaultDatabaseName => "dommeljsontests"; 10 | 11 | protected override async Task CreateTables() 12 | { 13 | using (var con = GetConnection(DefaultDatabaseName)) 14 | { 15 | var sql = @" 16 | CREATE TABLE IF NOT EXISTS `Leads` ( 17 | `Id` INT AUTO_INCREMENT PRIMARY KEY, 18 | `DateCreated` DATETIME, 19 | `Email` VARCHAR(255), 20 | `Data` LONGTEXT, 21 | `Metadata` LONGTEXT 22 | );"; 23 | await con.ExecuteScalarAsync(sql); 24 | } 25 | 26 | return await base.CreateTables(); 27 | } 28 | 29 | protected override async Task DropTables() 30 | { 31 | using (var con = GetConnection(DefaultDatabaseName)) 32 | { 33 | await con.ExecuteScalarAsync("DROP TABLE `Leads`"); 34 | } 35 | await base.DropTables(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Databases/JsonPostgresDatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Dapper; 3 | using Dommel.IntegrationTests; 4 | 5 | namespace Dommel.Json.IntegrationTests; 6 | 7 | public class JsonPostgresDatabaseDriver : PostgresDatabaseDriver 8 | { 9 | public override string DefaultDatabaseName => "dommeljsontests"; 10 | 11 | protected override async Task CreateTables() 12 | { 13 | using (var con = GetConnection(DefaultDatabaseName)) 14 | { 15 | var sql = @" 16 | create table if not exists ""Leads"" ( 17 | ""Id"" serial primary key, 18 | ""DateCreated"" timestamp, 19 | ""Email"" varchar(255), 20 | ""Data"" json, 21 | ""Metadata"" json 22 | );"; 23 | await con.ExecuteScalarAsync(sql); 24 | } 25 | 26 | return await base.CreateTables(); 27 | } 28 | 29 | protected override async Task DropTables() 30 | { 31 | using (var con = GetConnection(DefaultDatabaseName)) 32 | { 33 | await con.ExecuteScalarAsync(@"drop table ""Leads"""); 34 | } 35 | await base.DropTables(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Databases/JsonSqlServerDatabaseDriver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Dapper; 3 | using Dommel.IntegrationTests; 4 | 5 | namespace Dommel.Json.IntegrationTests; 6 | 7 | public class JsonSqlServerDatabaseDriver : SqlServerDatabaseDriver 8 | { 9 | public override string DefaultDatabaseName => "dommeljsontests"; 10 | 11 | protected override async Task CreateTables() 12 | { 13 | using (var con = GetConnection(DefaultDatabaseName)) 14 | { 15 | var sql = @" 16 | CREATE TABLE Leads ( 17 | Id INT IDENTITY(1,1) PRIMARY KEY, 18 | DateCreated DATETIME, 19 | Email VARCHAR(255), 20 | Data NVARCHAR(MAX), 21 | Metadata NVARCHAR(MAX) 22 | );"; 23 | await con.ExecuteScalarAsync(sql); 24 | } 25 | 26 | return await base.CreateTables(); 27 | } 28 | 29 | protected override async Task DropTables() 30 | { 31 | using (var con = GetConnection(DefaultDatabaseName)) 32 | { 33 | await con.ExecuteScalarAsync("DROP TABLE Leads"); 34 | } 35 | await base.DropTables(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/DeleteTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Threading.Tasks; 4 | using Dommel.IntegrationTests; 5 | using Xunit; 6 | 7 | namespace Dommel.Json.IntegrationTests; 8 | 9 | [Collection("JSON Database")] 10 | public class DeleteTests 11 | { 12 | private static object InsertLead(DbConnection con) 13 | { 14 | return con.Insert(new Lead 15 | { 16 | Email = "foo@example.com", 17 | Data = new LeadData 18 | { 19 | FirstName = "Foo", 20 | LastName = "Bar", 21 | Birthdate = new DateTime(1985, 7, 1), 22 | Email = "foo@example.com", 23 | } 24 | }); 25 | } 26 | 27 | private static async Task InsertLeadAsync(DbConnection con) 28 | { 29 | return await con.InsertAsync(new Lead 30 | { 31 | Email = "foo@example.com", 32 | Data = new LeadData 33 | { 34 | FirstName = "Foo", 35 | LastName = "Bar", 36 | Birthdate = new DateTime(1985, 7, 1), 37 | Email = "foo@example.com", 38 | } 39 | }); 40 | } 41 | 42 | [Theory] 43 | [ClassData(typeof(JsonDatabaseTestData))] 44 | public void SingleStatement(DatabaseDriver database) 45 | { 46 | using var con = database.GetConnection(); 47 | var id = InsertLead(con); 48 | Assert.True(con.DeleteMultiple(p => p.Data!.Email == "foo@example.com") > 0); 49 | var leads = con.Select(p => p.Data!.Email == "foo@example.com"); 50 | Assert.Empty(leads); 51 | } 52 | 53 | [Theory] 54 | [ClassData(typeof(JsonDatabaseTestData))] 55 | public async Task SingleStatementAsync(DatabaseDriver database) 56 | { 57 | using var con = database.GetConnection(); 58 | var id = await InsertLeadAsync(con); 59 | Assert.True(await con.DeleteMultipleAsync(p => p.Data!.Email == "foo@example.com") > 0); 60 | var leads = await con.SelectAsync(p => p.Data!.Email == "foo@example.com"); 61 | Assert.Empty(leads); 62 | } 63 | 64 | [Theory] 65 | [ClassData(typeof(JsonDatabaseTestData))] 66 | public void AndStatement(DatabaseDriver database) 67 | { 68 | using var con = database.GetConnection(); 69 | var id = InsertLead(con); 70 | Assert.True(con.DeleteMultiple(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" && p.Email == "foo@example.com") > 0); 71 | var leads = con.Select(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" && p.Email == "foo@example.com"); 72 | Assert.Empty(leads); 73 | } 74 | 75 | [Theory] 76 | [ClassData(typeof(JsonDatabaseTestData))] 77 | public async Task AndStatementAsync(DatabaseDriver database) 78 | { 79 | using var con = database.GetConnection(); 80 | var id = await InsertLeadAsync(con); 81 | Assert.True(await con.DeleteMultipleAsync(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" && p.Email == "foo@example.com") > 0); 82 | var leads = await con.SelectAsync(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" && p.Email == "foo@example.com"); 83 | Assert.Empty(leads); 84 | } 85 | 86 | [Theory] 87 | [ClassData(typeof(JsonDatabaseTestData))] 88 | public void OrStatement(DatabaseDriver database) 89 | { 90 | using var con = database.GetConnection(); 91 | var id = InsertLead(con); 92 | Assert.True(con.DeleteMultiple(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" || p.Email == "foo@example.com") > 0); 93 | var leads = con.Select(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" || p.Email == "foo@example.com"); 94 | Assert.Empty(leads); 95 | } 96 | 97 | [Theory] 98 | [ClassData(typeof(JsonDatabaseTestData))] 99 | public async Task OrStatementAsync(DatabaseDriver database) 100 | { 101 | using var con = database.GetConnection(); 102 | var id = await InsertLeadAsync(con); 103 | Assert.True(await con.DeleteMultipleAsync(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" || p.Email == "foo@example.com") > 0); 104 | var leads = await con.SelectAsync(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" || p.Email == "foo@example.com"); 105 | Assert.Empty(leads); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Dommel.Json.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | false 5 | 6 | 7 | 8 | 9 | runtime; build; native; contentfiles; analyzers 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Infrastructure/JsonDatabaseCollection.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Json.IntegrationTests; 4 | 5 | // Apply the text fixture to all tests in the "Database" collection 6 | [CollectionDefinition("JSON Database")] 7 | public class JsonDatabaseCollection : ICollectionFixture 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Infrastructure/JsonDatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Dapper; 4 | using Dommel.IntegrationTests; 5 | using Npgsql; 6 | using Xunit; 7 | 8 | namespace Dommel.Json.IntegrationTests; 9 | 10 | public class JsonDatabaseFixture : DatabaseFixtureBase 11 | { 12 | public JsonDatabaseFixture() 13 | { 14 | DommelJsonMapper.AddJson(new DommelJsonOptions 15 | { 16 | EntityAssemblies = new[] 17 | { 18 | typeof(JsonDatabaseFixture).Assembly, 19 | typeof(DatabaseFixture).Assembly 20 | }, 21 | JsonTypeHandler = () => new NpgJsonObjectTypeHandler(), 22 | }); 23 | } 24 | 25 | private class NpgJsonObjectTypeHandler : SqlMapper.ITypeHandler 26 | { 27 | private readonly JsonObjectTypeHandler _defaultTypeHandler = new(); 28 | 29 | public void SetValue(IDbDataParameter parameter, object value) 30 | { 31 | // Use the default handler 32 | _defaultTypeHandler.SetValue(parameter, value); 33 | 34 | // Set the special NpgsqlDbType to use the JSON data type 35 | if (parameter is NpgsqlParameter npgsqlParameter) 36 | { 37 | npgsqlParameter.NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Json; 38 | } 39 | } 40 | 41 | public object? Parse(Type destinationType, object value) => 42 | _defaultTypeHandler.Parse(destinationType, value); 43 | } 44 | 45 | protected override TheoryData Drivers => new JsonDatabaseTestData(); 46 | } 47 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Infrastructure/JsonDatabaseTestData.cs: -------------------------------------------------------------------------------- 1 | using Dommel.IntegrationTests; 2 | using Xunit; 3 | 4 | namespace Dommel.Json.IntegrationTests; 5 | 6 | public class JsonDatabaseTestData : TheoryData 7 | { 8 | public JsonDatabaseTestData() 9 | { 10 | // Defines the database providers to use for each test method. 11 | // These providers are used to initialize the databases in the 12 | // DatabaseFixture as well. 13 | if (!CI.IsTravis) 14 | { 15 | Add(new JsonSqlServerDatabaseDriver()); 16 | } 17 | 18 | Add(new JsonMySqlDatabaseDriver()); 19 | Add(new JsonPostgresDatabaseDriver()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/Models.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Dommel.Json.IntegrationTests; 5 | 6 | public class Lead 7 | { 8 | public int Id { get; set; } 9 | 10 | public DateTime DateCreated { get; set; } = DateTime.UtcNow; 11 | 12 | public string? Email { get; set; } 13 | 14 | [JsonData] 15 | public LeadData? Data { get; set; } 16 | 17 | [JsonData] 18 | public IDictionary? Metadata { get; set; } 19 | } 20 | 21 | public class LeadData 22 | { 23 | public string? FirstName { get; set; } 24 | 25 | public string? LastName { get; set; } 26 | 27 | public string? Email { get; set; } 28 | 29 | public DateTime Birthdate { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/SelectTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Common; 3 | using System.Threading.Tasks; 4 | using Dommel.IntegrationTests; 5 | using Xunit; 6 | 7 | namespace Dommel.Json.IntegrationTests; 8 | 9 | [Collection("JSON Database")] 10 | public class SelectTests 11 | { 12 | private static object InsertLead(DbConnection con) 13 | { 14 | return con.Insert(new Lead 15 | { 16 | Email = "foo@example.com", 17 | Data = new LeadData 18 | { 19 | FirstName = "Foo", 20 | LastName = "Bar", 21 | Birthdate = new DateTime(1985, 7, 1), 22 | Email = "foo@example.com", 23 | } 24 | }); 25 | } 26 | 27 | private static async Task InsertLeadAsync(DbConnection con) 28 | { 29 | return await con.InsertAsync(new Lead 30 | { 31 | Email = "foo@example.com", 32 | Data = new LeadData 33 | { 34 | FirstName = "Foo", 35 | LastName = "Bar", 36 | Birthdate = new DateTime(1985, 7, 1), 37 | Email = "foo@example.com", 38 | } 39 | }); 40 | } 41 | 42 | [Theory] 43 | [ClassData(typeof(JsonDatabaseTestData))] 44 | public void SelectSingleStatement(DatabaseDriver database) 45 | { 46 | using var con = database.GetConnection(); 47 | var id = InsertLead(con); 48 | var leads = con.Select(p => p.Data!.Email == "foo@example.com"); 49 | Assert.NotEmpty(leads); 50 | } 51 | 52 | [Theory] 53 | [ClassData(typeof(JsonDatabaseTestData))] 54 | public async Task SelectSingleStatementAsync(DatabaseDriver database) 55 | { 56 | using var con = database.GetConnection(); 57 | var id = await InsertLeadAsync(con); 58 | var leads = await con.SelectAsync(p => p.Data!.Email == "foo@example.com"); 59 | Assert.NotEmpty(leads); 60 | } 61 | 62 | [Theory] 63 | [ClassData(typeof(JsonDatabaseTestData))] 64 | public void SelectAndStatement(DatabaseDriver database) 65 | { 66 | using var con = database.GetConnection(); 67 | var id = InsertLead(con); 68 | var leads = con.Select(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" && p.Email == "foo@example.com"); 69 | Assert.NotEmpty(leads); 70 | } 71 | 72 | [Theory] 73 | [ClassData(typeof(JsonDatabaseTestData))] 74 | public async Task SelectAndStatementAsync(DatabaseDriver database) 75 | { 76 | using var con = database.GetConnection(); 77 | var id = await InsertLeadAsync(con); 78 | var leads = await con.SelectAsync(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" && p.Email == "foo@example.com"); 79 | Assert.NotEmpty(leads); 80 | } 81 | 82 | [Theory] 83 | [ClassData(typeof(JsonDatabaseTestData))] 84 | public void SelectOrStatement(DatabaseDriver database) 85 | { 86 | using var con = database.GetConnection(); 87 | var id = InsertLead(con); 88 | var leads = con.Select(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" || p.Email == "foo@example.com"); 89 | Assert.NotEmpty(leads); 90 | } 91 | 92 | [Theory] 93 | [ClassData(typeof(JsonDatabaseTestData))] 94 | public async Task SelectOrStatementAsync(DatabaseDriver database) 95 | { 96 | using var con = database.GetConnection(); 97 | var id = await InsertLeadAsync(con); 98 | var leads = await con.SelectAsync(p => p.Data!.FirstName == "Foo" && p.Data.LastName == "Bar" || p.Email == "foo@example.com"); 99 | Assert.NotEmpty(leads); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/Dommel.Json.IntegrationTests/UpdateTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using System.Threading.Tasks; 3 | using Dommel.IntegrationTests; 4 | using Xunit; 5 | 6 | namespace Dommel.Json.IntegrationTests; 7 | 8 | [Collection("JSON Database")] 9 | public class UpdateTests 10 | { 11 | private static Lead InsertLead(DbConnection con) 12 | { 13 | var id = con.Insert(new Lead 14 | { 15 | Data = new LeadData 16 | { 17 | FirstName = "Foo", 18 | } 19 | }); 20 | return con.Get(id)!; 21 | } 22 | 23 | private static async Task InsertLeadAsync(DbConnection con) 24 | { 25 | var id = await con.InsertAsync(new Lead 26 | { 27 | Data = new LeadData 28 | { 29 | FirstName = "Foo", 30 | } 31 | }); 32 | return (await con.GetAsync(id))!; 33 | } 34 | 35 | [Theory] 36 | [ClassData(typeof(JsonDatabaseTestData))] 37 | public void SelectSingleStatement(DatabaseDriver database) 38 | { 39 | using var con = database.GetConnection(); 40 | // Arrange 41 | var lead = InsertLead(con); 42 | 43 | // Act 44 | lead.Data!.FirstName = "Bar"; 45 | con.Update(lead); 46 | 47 | // Assert 48 | var updatedLead = con.Get(lead.Id); 49 | Assert.NotNull(updatedLead); 50 | Assert.Equal("Bar", updatedLead!.Data?.FirstName); 51 | } 52 | 53 | [Theory] 54 | [ClassData(typeof(JsonDatabaseTestData))] 55 | public async Task SelectSingleStatementAsync(DatabaseDriver database) 56 | { 57 | using var con = database.GetConnection(); 58 | 59 | // Arrange 60 | var lead = await InsertLeadAsync(con); 61 | 62 | // Act 63 | lead.Data!.FirstName = "Bar"; 64 | con.Update(lead); 65 | 66 | // Assert 67 | var updatedLead = con.Get(lead.Id); 68 | Assert.NotNull(updatedLead); 69 | Assert.Equal("Bar", updatedLead!.Data?.FirstName); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/Dommel.Json.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | false 5 | 6 | 7 | 8 | 9 | runtime; build; native; contentfiles; analyzers 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/DommelJsonMapperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Reflection; 5 | using Dapper; 6 | using Xunit; 7 | 8 | namespace Dommel.Json.Tests; 9 | 10 | public class DommelJsonMapperTests 11 | { 12 | public DommelJsonMapperTests() 13 | { 14 | DommelJsonMapper.AddJson(typeof(Lead).Assembly); 15 | } 16 | 17 | [Fact] 18 | public void AddsJsonPropertyResolver() 19 | { 20 | var propertyResolver = Assert.IsType(DommelMapper.PropertyResolver); 21 | Assert.Equal(typeof(LeadData), Assert.Single(propertyResolver.JsonTypes)); 22 | } 23 | 24 | [Fact] 25 | public void AddsJsonSqlBuilders() 26 | { 27 | Assert.IsType(DommelMapper.SqlBuilders["sqlconnection"]); 28 | Assert.IsType(DommelMapper.SqlBuilders["sqlceconnection"]); 29 | Assert.IsType(DommelMapper.SqlBuilders["sqliteconnection"]); 30 | Assert.IsType(DommelMapper.SqlBuilders["npgsqlconnection"]); 31 | Assert.IsType(DommelMapper.SqlBuilders["mysqlconnection"]); 32 | } 33 | 34 | [Fact] 35 | public void AddsCustomSqlExpressionFactory() 36 | { 37 | Assert.IsType>( 38 | DommelMapper.SqlExpressionFactory.Invoke(typeof(Lead), new MySqlSqlBuilder())); 39 | } 40 | 41 | [Fact] 42 | public void ThrowsWhenNotJsonSqlBuilder() 43 | { 44 | var ex = Assert.Throws( 45 | () => DommelMapper.SqlExpressionFactory.Invoke(typeof(Lead), new Dommel.MySqlSqlBuilder())); 46 | Assert.Equal($"The specified SQL builder type should be assignable from {nameof(IJsonSqlBuilder)}.", ex.Message); 47 | } 48 | 49 | 50 | [Fact] 51 | public void AddsDapperTypeHandler() 52 | { 53 | var typeHandlers = GetDapperTypeHandlers(); 54 | Assert.Contains(typeHandlers, kvp => kvp.Value is JsonObjectTypeHandler); 55 | } 56 | 57 | // Dirty hack to determine whether the Dapper type handler has been added 58 | private static Dictionary GetDapperTypeHandlers() => 59 | (Dictionary)typeof(SqlMapper).GetField("typeHandlers", BindingFlags.Static | BindingFlags.NonPublic)!.GetValue(null)!; 60 | 61 | [Fact] 62 | public void AddsCustomJsonTypeHandler() 63 | { 64 | // Act 65 | DommelJsonMapper.AddJson(new DommelJsonOptions 66 | { 67 | EntityAssemblies = new[] { typeof(Lead).Assembly }, 68 | JsonTypeHandler = () => new CustomJsonTypeHandler() 69 | }); 70 | 71 | // Assert 72 | var typeHandlers = GetDapperTypeHandlers(); 73 | Assert.Contains(typeHandlers, kvp => kvp.Value is CustomJsonTypeHandler); 74 | 75 | } 76 | 77 | private class CustomJsonTypeHandler : SqlMapper.ITypeHandler 78 | { 79 | public object Parse(Type destinationType, object value) => throw new NotImplementedException(); 80 | public void SetValue(IDbDataParameter parameter, object value) => throw new NotImplementedException(); 81 | } 82 | 83 | [Fact] 84 | public void ThrowsWhenNullOptions() => 85 | Assert.Throws("options", () => DommelJsonMapper.AddJson(options: null!)); 86 | 87 | [Fact] 88 | public void ThrowsWhenNullEntityAssemblies() 89 | { 90 | var ex = Assert.Throws("options", () => DommelJsonMapper.AddJson(new DommelJsonOptions())); 91 | Assert.Equal(new ArgumentException("No entity assemblies specified.", "options").Message, ex.Message); 92 | } 93 | 94 | [Fact] 95 | public void ThrowsWhenEmptyEntityAssemblies() 96 | { 97 | var ex = Assert.Throws("options", () => DommelJsonMapper.AddJson(new DommelJsonOptions { EntityAssemblies = Array.Empty() })); 98 | Assert.Equal(new ArgumentException("No entity assemblies specified.", "options").Message, ex.Message); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/DommelJsonOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Json.Tests; 4 | 5 | public class DommelJsonOptionsTests 6 | { 7 | [Fact] 8 | public void SetsEntityAssemblies() 9 | { 10 | // Arrange 11 | var assemblies = new[] { typeof(DommelJsonOptions).Assembly }; 12 | 13 | // Act 14 | var options = new DommelJsonOptions { EntityAssemblies = assemblies }; 15 | 16 | // Assert 17 | Assert.Equal(assemblies, options.EntityAssemblies); 18 | Assert.Null(options.JsonTypeHandler); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/JsonObjectTypeHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Microsoft.Data.SqlClient; 4 | using Newtonsoft.Json; 5 | using Xunit; 6 | 7 | namespace Dommel.Json.Tests; 8 | 9 | public class JsonObjectTypeHandlerTests 10 | { 11 | private static readonly JsonObjectTypeHandler TypeHandler = new(); 12 | 13 | [Fact] 14 | public void SetValue_CreatesJsonString() 15 | { 16 | var obj = new 17 | { 18 | Foo = "Bar", 19 | Baz = 123 20 | }; 21 | var param = new SqlParameter(); 22 | 23 | // Act 24 | TypeHandler.SetValue(param, obj); 25 | 26 | // Assert 27 | var json = JsonConvert.SerializeObject(obj); 28 | Assert.Equal(json, param.Value); 29 | Assert.Equal(DbType.String, param.DbType); 30 | } 31 | 32 | [Fact] 33 | public void SetValue_HandlesNull() 34 | { 35 | // Arrange 36 | var param = new SqlParameter(); 37 | 38 | // Act 39 | TypeHandler.SetValue(param, null); 40 | 41 | // Assert 42 | Assert.Equal(DBNull.Value, param.Value); 43 | Assert.Equal(DbType.String, param.DbType); 44 | } 45 | 46 | [Fact] 47 | public void SetValue_HandlesDBNull() 48 | { 49 | // Arrange 50 | var param = new SqlParameter(); 51 | 52 | // Act 53 | TypeHandler.SetValue(param, DBNull.Value); 54 | 55 | // Assert 56 | Assert.Equal(DBNull.Value, param.Value); 57 | Assert.Equal(DbType.String, param.DbType); 58 | } 59 | 60 | [Fact] 61 | public void Parse_DeserializesJsonString() 62 | { 63 | // Arrange 64 | var data = new LeadData 65 | { 66 | FirstName = "Foo", 67 | LastName = "Bar", 68 | Birthdate = new DateTime(1985, 7, 1), 69 | Email = "foo@example.com", 70 | }; 71 | var json = JsonConvert.SerializeObject(data); 72 | 73 | // Act 74 | var obj = TypeHandler.Parse(typeof(LeadData), json); 75 | 76 | // Assert 77 | var parsedData = Assert.IsType(obj); 78 | Assert.Equal(data.FirstName, parsedData.FirstName); 79 | Assert.Equal(data.LastName, parsedData.LastName); 80 | Assert.Equal(data.Birthdate, parsedData.Birthdate); 81 | Assert.Equal(data.Email, parsedData.Email); 82 | } 83 | 84 | [Theory] 85 | [InlineData(null)] 86 | [InlineData(123)] 87 | [InlineData(123.4)] 88 | public void Parse_ReturnsNullForNonString(object? value) => Assert.Null(TypeHandler.Parse(typeof(LeadData), value)); 89 | } 90 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/JsonPropertyResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace Dommel.Json.Tests; 6 | 7 | public class JsonPropertyResolverTests 8 | { 9 | private static readonly Type LeadType = typeof(Lead); 10 | 11 | [Fact] 12 | public void DefaultBehavior() 13 | { 14 | // Arrange 15 | var resolver = new JsonPropertyResolver(Array.Empty()); 16 | 17 | // Act 18 | var props = resolver.ResolveProperties(LeadType).Select(x => x.Property); 19 | 20 | // Assert 21 | Assert.Collection( 22 | props, 23 | p => Assert.Equal(p, LeadType.GetProperty("Id")), 24 | p => Assert.Equal(p, LeadType.GetProperty("DateCreated")), 25 | p => Assert.Equal(p, LeadType.GetProperty("Email")) 26 | ); 27 | } 28 | 29 | [Fact] 30 | public void ResolvesComplexType() 31 | { 32 | // Arrange 33 | var resolver = new JsonPropertyResolver(new[] { typeof(LeadData) }); 34 | 35 | // Act 36 | var props = resolver.ResolveProperties(LeadType).Select(x => x.Property); 37 | 38 | // Assert 39 | Assert.Collection( 40 | props, 41 | p => Assert.Equal(p, LeadType.GetProperty("Id")), 42 | p => Assert.Equal(p, LeadType.GetProperty("DateCreated")), 43 | p => Assert.Equal(p, LeadType.GetProperty("Email")), 44 | p => Assert.Equal(p, LeadType.GetProperty("Data")) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/JsonSqlExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Json.Tests; 4 | 5 | public class JsonSqlExpressionTests 6 | { 7 | [Fact] 8 | public void GeneratesMySqlJsonValue() 9 | { 10 | // Arrange 11 | var sql = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 12 | 13 | // Act 14 | var str = sql.Where(p => p.Data.LastName == "Foo").ToSql(out var parameters); 15 | 16 | // Assert 17 | Assert.Equal(" where `Leads`.`Data`->'$.LastName' = @p1", str); 18 | Assert.Equal("p1", Assert.Single(parameters.ParameterNames)); 19 | } 20 | 21 | [Fact] 22 | public void GeneratesSqlServerJsonValue() 23 | { 24 | // Arrange 25 | var sql = new JsonSqlExpression(new SqlServerSqlBuilder(), new DommelJsonOptions()); 26 | 27 | // Act 28 | var str = sql.Where(p => p.Data.LastName == "Foo").ToSql(out var parameters); 29 | 30 | // Assert 31 | Assert.Equal(" where JSON_VALUE([Leads].[Data], '$.LastName') = @p1", str); 32 | Assert.Equal("p1", Assert.Single(parameters.ParameterNames)); 33 | } 34 | 35 | [Fact] 36 | public void GeneratesSqliteJsonValue() 37 | { 38 | // Arrange 39 | var sql = new JsonSqlExpression(new SqliteSqlBuilder(), new DommelJsonOptions()); 40 | 41 | // Act 42 | var str = sql.Where(p => p.Data.LastName == "Foo").ToSql(out var parameters); 43 | 44 | // Assert 45 | Assert.Equal(" where JSON_EXTRACT(Leads.Data, '$.LastName') = @p1", str); 46 | Assert.Equal("p1", Assert.Single(parameters.ParameterNames)); 47 | } 48 | 49 | [Fact] 50 | public void GeneratesSqlServerCeJsonValue() 51 | { 52 | // Arrange 53 | var sql = new JsonSqlExpression(new SqlServerCeSqlBuilder(), new DommelJsonOptions()); 54 | 55 | // Act 56 | var str = sql.Where(p => p.Data.LastName == "Foo").ToSql(out var parameters); 57 | 58 | // Assert 59 | Assert.Equal(" where JSON_VALUE([Leads].[Data], '$.LastName') = @p1", str); 60 | Assert.Equal("p1", Assert.Single(parameters.ParameterNames)); 61 | } 62 | 63 | [Fact] 64 | public void GeneratesPostgresJsonValue() 65 | { 66 | // Arrange 67 | var sql = new JsonSqlExpression(new PostgresSqlBuilder(), new DommelJsonOptions()); 68 | 69 | // Act 70 | var str = sql.Where(p => p.Data.LastName == "Foo").ToSql(out var parameters); 71 | 72 | // Assert 73 | Assert.Equal(" where \"Leads\".\"Data\"->>'LastName' = @p1", str); 74 | Assert.Equal("p1", Assert.Single(parameters.ParameterNames)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/LikeTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Xunit; 3 | 4 | namespace Dommel.Json.Tests; 5 | 6 | public class LikeTests 7 | { 8 | [Fact] 9 | public void LikeOperandContains_WithConstant() 10 | { 11 | // Arrange 12 | var sqlExpression = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 13 | 14 | // Act 15 | var expression = sqlExpression.Where(p => p.Data.FirstName!.Contains("test")); 16 | var sql = expression.ToSql(out var dynamicParameters); 17 | 18 | // Assert 19 | Assert.Equal("where `Leads`.`Data`->'$.FirstName' like @p1", sql.Trim()); 20 | Assert.Single(dynamicParameters.ParameterNames); 21 | Assert.Equal("%test%", dynamicParameters.Get("p1")); 22 | } 23 | 24 | [Fact] 25 | public void LikeOperand_WithVariable() 26 | { 27 | // Arrange 28 | var sqlExpression = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 29 | var substring = "test"; 30 | 31 | // Act 32 | var expression = sqlExpression.Where(p => p.Data.FirstName!.Contains(substring)); 33 | var sql = expression.ToSql(out var dynamicParameters); 34 | 35 | // Assert 36 | Assert.Equal("where `Leads`.`Data`->'$.FirstName' like @p1", sql.Trim()); 37 | Assert.Single(dynamicParameters.ParameterNames); 38 | Assert.Equal("%test%", dynamicParameters.Get("p1")); 39 | } 40 | 41 | [Fact] 42 | public void LikeOperandStartsWith_WithConstant() 43 | { 44 | // Arrange 45 | var sqlExpression = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 46 | 47 | // Act 48 | var expression = sqlExpression.Where(p => p.Data.FirstName!.StartsWith("test")); 49 | var sql = expression.ToSql(out var dynamicParameters); 50 | 51 | // Assert 52 | Assert.Equal("where `Leads`.`Data`->'$.FirstName' like @p1", sql.Trim()); 53 | Assert.Single(dynamicParameters.ParameterNames); 54 | Assert.Equal("test%", dynamicParameters.Get("p1")); 55 | } 56 | 57 | [Fact] 58 | public void LikeOperandStartsWith_WithVariable() 59 | { 60 | // Arrange 61 | var sqlExpression = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 62 | var substring = "test"; 63 | 64 | // Act 65 | var expression = sqlExpression.Where(p => p.Data.FirstName!.StartsWith(substring)); 66 | var sql = expression.ToSql(out var dynamicParameters); 67 | 68 | // Assert 69 | Assert.Equal("where `Leads`.`Data`->'$.FirstName' like @p1", sql.Trim()); 70 | Assert.Single(dynamicParameters.ParameterNames); 71 | Assert.Equal("test%", dynamicParameters.Get("p1")); 72 | } 73 | 74 | [Fact] 75 | public void LikeOperandEndsWith_WithConstant() 76 | { 77 | // Arrange 78 | var sqlExpression = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 79 | 80 | // Act 81 | var expression = sqlExpression.Where(p => p.Data.FirstName!.EndsWith("test")); 82 | var sql = expression.ToSql(out var dynamicParameters); 83 | 84 | // Assert 85 | Assert.Equal("where `Leads`.`Data`->'$.FirstName' like @p1", sql.Trim()); 86 | Assert.Single(dynamicParameters.ParameterNames); 87 | Assert.Equal("%test", dynamicParameters.Get("p1")); 88 | } 89 | 90 | [Fact] 91 | public void LikeOperandEndsWith_WithVariable() 92 | { 93 | // Arrange 94 | var sqlExpression = new JsonSqlExpression(new MySqlSqlBuilder(), new DommelJsonOptions()); 95 | var substring = "test"; 96 | 97 | // Act 98 | var expression = sqlExpression.Where(p => p.Data.FirstName!.EndsWith(substring)); 99 | var sql = expression.ToSql(out var dynamicParameters); 100 | 101 | // Assert 102 | Assert.Equal("where `Leads`.`Data`->'$.FirstName' like @p1", sql.Trim()); 103 | Assert.Single(dynamicParameters.ParameterNames); 104 | Assert.Equal("%test", dynamicParameters.Get("p1")); 105 | } 106 | 107 | [Table("tblFoo")] 108 | public class Foo 109 | { 110 | public int Id { get; set; } 111 | 112 | public string Bar { get; set; } = ""; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/Dommel.Json.Tests/Models.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel.Json.Tests; 4 | 5 | public class Lead 6 | { 7 | public int Id { get; set; } 8 | 9 | public DateTime DateCreated { get; set; } = DateTime.UtcNow; 10 | 11 | public string? Email { get; set; } 12 | 13 | [JsonData] 14 | public LeadData Data { get; set; } = new LeadData(); 15 | } 16 | 17 | public class LeadData 18 | { 19 | public string? FirstName { get; set; } 20 | 21 | public string? LastName { get; set; } 22 | 23 | public string? Email { get; set; } 24 | 25 | public DateTime Birthdate { get; set; } 26 | } 27 | -------------------------------------------------------------------------------- /test/Dommel.Tests/AnyTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using static Dommel.DommelMapper; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class AnyTests 7 | { 8 | private static readonly ISqlBuilder SqlBuilder = new SqlServerSqlBuilder(); 9 | 10 | [Fact] 11 | public void GeneratesAnyAllSql() 12 | { 13 | var sql = BuildAnyAllSql(SqlBuilder, typeof(Foo)); 14 | Assert.Equal($"select 1 from [Foos] {SqlBuilder.LimitClause(1)}", sql); 15 | } 16 | 17 | [Fact] 18 | public void GeneratesAnySql() 19 | { 20 | var sql = BuildAnySql(SqlBuilder, x => x.Bar == "Baz", out var parameters); 21 | Assert.Equal($"select 1 from [Foos] where [Foos].[Bar] = @p1 {SqlBuilder.LimitClause(1)}", sql); 22 | Assert.Single(parameters.ParameterNames); 23 | } 24 | 25 | private class Foo 26 | { 27 | public string? Bar { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/Dommel.Tests/CacheTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class CacheTests 6 | { 7 | [Theory] 8 | [InlineData(QueryCacheType.Get)] 9 | [InlineData(QueryCacheType.GetByMultipleIds)] 10 | [InlineData(QueryCacheType.GetAll)] 11 | [InlineData(QueryCacheType.Project)] 12 | [InlineData(QueryCacheType.ProjectAll)] 13 | [InlineData(QueryCacheType.Count)] 14 | [InlineData(QueryCacheType.Insert)] 15 | [InlineData(QueryCacheType.Update)] 16 | [InlineData(QueryCacheType.Delete)] 17 | [InlineData(QueryCacheType.DeleteAll)] 18 | [InlineData(QueryCacheType.Any)] 19 | internal void SetsCache(QueryCacheType queryCacheType) 20 | { 21 | var cacheKey = new QueryCacheKey(queryCacheType, new DummySqlBuilder(), typeof(Foo)); 22 | DommelMapper.QueryCache[cacheKey] = "blah"; 23 | Assert.Equal("blah", DommelMapper.QueryCache[cacheKey]); 24 | } 25 | 26 | [Fact] 27 | public void IsEqual() 28 | { 29 | Assert.Equal( 30 | new QueryCacheKey(QueryCacheType.Get, new DummySqlBuilder(), typeof(Foo)), 31 | new QueryCacheKey(QueryCacheType.Get, new DummySqlBuilder(), typeof(Foo))); 32 | } 33 | 34 | [Fact] 35 | public void IsNotEqualCacheType() 36 | { 37 | Assert.NotEqual( 38 | new QueryCacheKey(QueryCacheType.Get, new DummySqlBuilder(), typeof(Foo)), 39 | new QueryCacheKey(QueryCacheType.GetAll, new DummySqlBuilder(), typeof(Foo))); 40 | } 41 | 42 | [Fact] 43 | public void IsNotEqualBuilderType() 44 | { 45 | Assert.NotEqual( 46 | new QueryCacheKey(QueryCacheType.Get, new SqlServerSqlBuilder(), typeof(Foo)), 47 | new QueryCacheKey(QueryCacheType.Get, new DummySqlBuilder(), typeof(Foo))); 48 | } 49 | 50 | [Fact] 51 | public void IsNotEqualEntityType() 52 | { 53 | Assert.NotEqual( 54 | new QueryCacheKey(QueryCacheType.Get, new DummySqlBuilder(), typeof(Foo)), 55 | new QueryCacheKey(QueryCacheType.Get, new DummySqlBuilder(), typeof(Bar))); 56 | } 57 | 58 | private class Foo { } 59 | private class Bar { } 60 | } 61 | -------------------------------------------------------------------------------- /test/Dommel.Tests/ColumnPropertyInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Xunit; 4 | 5 | namespace Dommel.Tests; 6 | 7 | public class ColumnPropertyInfoTests 8 | { 9 | [Fact] 10 | public void ThrowsForNullPropertyInfo() 11 | { 12 | Assert.Throws("property", () => new ColumnPropertyInfo(null!)); 13 | Assert.Throws("property", () => new ColumnPropertyInfo(null!, isKey: false)); 14 | Assert.Throws("property", () => new ColumnPropertyInfo(null!, isKey: true)); 15 | Assert.Throws("property", () => new ColumnPropertyInfo(null!, default(DatabaseGeneratedOption))); 16 | } 17 | 18 | [Fact] 19 | public void UsesGeneratedOptionsNone_ForRegularProperties() 20 | { 21 | var cpi = new ColumnPropertyInfo(typeof(Foo).GetProperty("Name")!); 22 | Assert.Equal(DatabaseGeneratedOption.None, cpi.GeneratedOption); 23 | Assert.False(cpi.IsGenerated); 24 | Assert.Equal(typeof(Foo).GetProperty("Name"), cpi.Property); 25 | } 26 | 27 | [Fact] 28 | public void UsesGeneratedOptionsIdentity_ForKeyProperties() 29 | { 30 | var cpi = new ColumnPropertyInfo(typeof(Foo).GetProperty("Id")!, isKey: true); 31 | Assert.Equal(DatabaseGeneratedOption.Identity, cpi.GeneratedOption); 32 | Assert.True(cpi.IsGenerated); 33 | Assert.Equal(typeof(Foo).GetProperty("Id"), cpi.Property); 34 | } 35 | 36 | [Theory] 37 | [InlineData(true)] 38 | [InlineData(false)] 39 | public void DeterminesDatabaseGeneratedOption_Computed(bool isKey) 40 | { 41 | var cpi = new ColumnPropertyInfo(typeof(Bar).GetProperty("Id")!, isKey); 42 | Assert.Equal(DatabaseGeneratedOption.Computed, cpi.GeneratedOption); 43 | Assert.True(cpi.IsGenerated); 44 | Assert.Equal(typeof(Bar).GetProperty("Id"), cpi.Property); 45 | } 46 | 47 | [Theory] 48 | [InlineData(true)] 49 | [InlineData(false)] 50 | public void DeterminesDatabaseGeneratedOption_None(bool isKey) 51 | { 52 | var cpi = new ColumnPropertyInfo(typeof(Baz).GetProperty("Id")!, isKey); 53 | Assert.Equal(DatabaseGeneratedOption.None, cpi.GeneratedOption); 54 | Assert.False(cpi.IsGenerated); 55 | Assert.Equal(typeof(Baz).GetProperty("Id"), cpi.Property); 56 | } 57 | 58 | [Theory] 59 | [InlineData(DatabaseGeneratedOption.None)] 60 | [InlineData(DatabaseGeneratedOption.Identity)] 61 | [InlineData(DatabaseGeneratedOption.Computed)] 62 | public void UsesSpecifiedDatabaseGeneratedOption(DatabaseGeneratedOption databaseGeneratedOption) 63 | { 64 | var kpi = new ColumnPropertyInfo(typeof(Bar).GetProperty("Id")!, databaseGeneratedOption); 65 | Assert.Equal(databaseGeneratedOption, kpi.GeneratedOption); 66 | Assert.Equal(typeof(Bar).GetProperty("Id"), kpi.Property); 67 | } 68 | 69 | private class Foo 70 | { 71 | public int Id { get; set; } 72 | 73 | public string? Name { get; set; } 74 | } 75 | 76 | private class Bar 77 | { 78 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 79 | public Guid Id { get; set; } 80 | } 81 | 82 | private class Baz 83 | { 84 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 85 | public Guid Id { get; set; } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/Dommel.Tests/CountTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using static Dommel.DommelMapper; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class CountTests 7 | { 8 | private static readonly ISqlBuilder SqlBuilder = new SqlServerSqlBuilder(); 9 | 10 | [Fact] 11 | public void GeneratesCountAllSql() 12 | { 13 | var sql = BuildCountAllSql(SqlBuilder, typeof(Foo)); 14 | Assert.Equal("select count(*) from [Foos]", sql); 15 | } 16 | 17 | [Fact] 18 | public void GeneratesCountSql() 19 | { 20 | var sql = BuildCountSql(SqlBuilder, x => x.Bar == "Baz", out var parameters); 21 | Assert.Equal("select count(*) from [Foos] where [Foos].[Bar] = @p1", sql); 22 | Assert.Single(parameters.ParameterNames); 23 | } 24 | 25 | private class Foo 26 | { 27 | public string? Bar { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/Dommel.Tests/DefaultColumnNameResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Xunit; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class DefaultColumnNameResolverTests 7 | { 8 | private static readonly DefaultColumnNameResolver Resolver = new(); 9 | 10 | [Fact] 11 | public void ResolvesName() 12 | { 13 | var name = Resolver.ResolveColumnName(typeof(Foo).GetProperty("Bar")!); 14 | Assert.Equal("Bar", name); 15 | } 16 | 17 | [Fact] 18 | public void ResolvesColumnAttribute() 19 | { 20 | var name = Resolver.ResolveColumnName(typeof(Bar).GetProperty("FooBarBaz")!); 21 | Assert.Equal("foo_bar_baz", name); 22 | } 23 | 24 | private class Foo 25 | { 26 | public string? Bar { get; set; } 27 | } 28 | 29 | private class Bar 30 | { 31 | [Column("foo_bar_baz")] 32 | public string? FooBarBaz { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/Dommel.Tests/DefaultForeignKeyPropertyResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using Xunit; 5 | 6 | namespace Dommel.Tests; 7 | 8 | public class DefaultForeignKeyPropertyResolverTests 9 | { 10 | [Fact] 11 | public void Resolves_ThrowsWhenUnableToFind() 12 | { 13 | var resolver = new DefaultForeignKeyPropertyResolver(); 14 | var fk = Assert.Throws(() => resolver.ResolveForeignKeyProperty(typeof(Product), typeof(Product), out var fkRelation)); 15 | Assert.Equal("Could not resolve foreign key property. Source type 'Dommel.Tests.Product'; including type: 'Dommel.Tests.Product'.", fk.Message); 16 | } 17 | 18 | [Fact] 19 | public void Resolves_OneToOne_WithDefaultConventions() 20 | { 21 | // Arrange 22 | var resolver = new DefaultForeignKeyPropertyResolver(); 23 | 24 | // Act 25 | var fk = resolver.ResolveForeignKeyProperty(typeof(Product), typeof(Category), out var fkRelation); 26 | 27 | // Assert 28 | Assert.Equal(typeof(Product).GetProperty(nameof(Product.CategoryId)), fk); 29 | Assert.Equal(ForeignKeyRelation.OneToOne, fkRelation); 30 | } 31 | 32 | [Fact] 33 | public void Resolves_OneToMany_WithDefaultConventions() 34 | { 35 | // Arrange 36 | var resolver = new DefaultForeignKeyPropertyResolver(); 37 | 38 | // Act 39 | var fk = resolver.ResolveForeignKeyProperty(typeof(Product), typeof(ProductOption), out var fkRelation); 40 | 41 | // Assert 42 | Assert.Equal(typeof(ProductOption).GetProperty(nameof(ProductOption.ProductId)), fk); 43 | Assert.Equal(ForeignKeyRelation.OneToMany, fkRelation); 44 | } 45 | 46 | [Fact] 47 | public void Resolves_OneToOne_WithAttributes() 48 | { 49 | // Arrange 50 | var resolver = new DefaultForeignKeyPropertyResolver(); 51 | 52 | // Act 53 | var fk = resolver.ResolveForeignKeyProperty(typeof(ProductDto), typeof(CategoryDto), out var fkRelation); 54 | 55 | // Assert 56 | Assert.Equal(typeof(ProductDto).GetProperty(nameof(ProductDto.CategoryId)), fk); 57 | Assert.Equal(ForeignKeyRelation.OneToOne, fkRelation); 58 | } 59 | 60 | [Fact] 61 | public void Resolves_OneToMany_WithAttributes() 62 | { 63 | // Arrange 64 | var resolver = new DefaultForeignKeyPropertyResolver(); 65 | 66 | // Act 67 | var fk = resolver.ResolveForeignKeyProperty(typeof(ProductDto), typeof(ProductOptionDto), out var fkRelation); 68 | 69 | // Assert 70 | Assert.Equal(typeof(ProductOptionDto).GetProperty(nameof(ProductOptionDto.ProductId)), fk); 71 | Assert.Equal(ForeignKeyRelation.OneToMany, fkRelation); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/Dommel.Tests/DefaultKeyPropertyResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace Dommel.Tests; 8 | 9 | public class DefaultKeyPropertyResolverTests 10 | { 11 | private static readonly IKeyPropertyResolver Resolver = new DefaultKeyPropertyResolver(); 12 | 13 | [Fact] 14 | public void MapsIdProperty() 15 | { 16 | var prop = Resolver.ResolveKeyProperties(typeof(Foo)).Single(); 17 | Assert.Equal(typeof(Foo).GetProperty("Id"), prop.Property); 18 | Assert.Equal(DatabaseGeneratedOption.Identity, prop.GeneratedOption); 19 | Assert.True(prop.IsGenerated); 20 | } 21 | 22 | [Fact] 23 | public void MapsIdPropertyInheritance() 24 | { 25 | var prop = Resolver.ResolveKeyProperties(typeof(FooInheritance)).Single().Property; 26 | Assert.Equal(typeof(FooInheritance).GetProperty("Id"), prop); 27 | } 28 | 29 | 30 | [Fact] 31 | public void MapsIdPropertyGenericInheritance() 32 | { 33 | var prop = Resolver.ResolveKeyProperties(typeof(FooGenericInheritance)).Single().Property; 34 | Assert.Equal(typeof(FooGenericInheritance).GetProperty("Id"), prop); 35 | } 36 | 37 | 38 | 39 | [Fact] 40 | public void MapsWithAttribute() 41 | { 42 | var prop = Resolver.ResolveKeyProperties(typeof(Bar)).Single().Property; 43 | Assert.Equal(typeof(Bar).GetProperty("BarId"), prop); 44 | } 45 | 46 | [Fact] 47 | public void NoKeyProperties_ThrowsException() 48 | { 49 | var ex = Assert.Throws(() => Resolver.ResolveKeyProperties(typeof(Nope)).Single().Property); 50 | Assert.Equal($"Could not find the key properties for type '{typeof(Nope).FullName}'.", ex.Message); 51 | } 52 | 53 | [Fact] 54 | public void MapsMultipleKeyProperties() 55 | { 56 | var keyProperties = Resolver.ResolveKeyProperties(typeof(FooBar)); 57 | Assert.Equal(2, keyProperties.Length); 58 | Assert.Equal(typeof(FooBar).GetProperty("Id"), keyProperties[0].Property); 59 | Assert.Equal(typeof(FooBar).GetProperty("BarId"), keyProperties[1].Property); 60 | } 61 | 62 | [Fact] 63 | public void MapsNonGeneratedId() 64 | { 65 | var keyProperty = Resolver.ResolveKeyProperties(typeof(WithNonGeneratedIdColumn)).Single(); 66 | Assert.Equal(typeof(WithNonGeneratedIdColumn).GetProperty("Id"), keyProperty.Property); 67 | Assert.Equal(DatabaseGeneratedOption.None, keyProperty.GeneratedOption); 68 | Assert.False(keyProperty.IsGenerated); 69 | } 70 | 71 | [Fact] 72 | public void MapsNonGeneratedIdWithCustomColumnName() 73 | { 74 | var keyProperty = Resolver.ResolveKeyProperties(typeof(WithNonGeneratedCustomIdColumn)).Single(); 75 | Assert.Equal(typeof(WithNonGeneratedCustomIdColumn).GetProperty("MyNonGeneratedKey"), keyProperty.Property); 76 | Assert.Equal(DatabaseGeneratedOption.None, keyProperty.GeneratedOption); 77 | Assert.False(keyProperty.IsGenerated); 78 | } 79 | 80 | private class FooGeneric where T : struct 81 | { 82 | public T Id { get; set; } 83 | } 84 | 85 | 86 | private class FooGenericInheritance : FooGeneric 87 | { 88 | } 89 | 90 | private class FooInheritance : Foo 91 | { 92 | } 93 | 94 | 95 | private class Foo 96 | { 97 | public object? Id { get; set; } 98 | } 99 | 100 | private class Bar 101 | { 102 | [Key] 103 | public object? BarId { get; set; } 104 | } 105 | 106 | private class FooBar 107 | { 108 | public object? Id { get; set; } 109 | 110 | [Key] 111 | public object? BarId { get; set; } 112 | } 113 | 114 | private class Nope 115 | { 116 | public object? Foo { get; set; } 117 | } 118 | 119 | private class WithNonGeneratedIdColumn 120 | { 121 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 122 | public Guid Id { get; set; } 123 | } 124 | 125 | private class WithNonGeneratedCustomIdColumn 126 | { 127 | [Key] 128 | [DatabaseGenerated(DatabaseGeneratedOption.None)] 129 | public Guid MyNonGeneratedKey { get; set; } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/Dommel.Tests/DefaultTableNameResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | using Xunit; 4 | 5 | namespace Dommel.Tests; 6 | 7 | public class DefaultTableNameResolverTests 8 | { 9 | private static readonly DefaultTableNameResolver Resolver = new(); 10 | 11 | [Theory] 12 | [InlineData(typeof(Product), "Products")] 13 | [InlineData(typeof(Products), "Products")] 14 | [InlineData(typeof(Category), "Categories")] 15 | public void PluralizesName(Type type, string tableName) 16 | { 17 | var name = Resolver.ResolveTableName(type); 18 | Assert.Equal(tableName, name); 19 | } 20 | 21 | [Fact] 22 | public void MapsTableAttribute() 23 | { 24 | var name = Resolver.ResolveTableName(typeof(Foo)); 25 | Assert.Equal("tblFoo", name); 26 | } 27 | 28 | [Fact] 29 | public void MapsTableAttributeWithSchema() 30 | { 31 | var name = Resolver.ResolveTableName(typeof(FooWithSchema)); 32 | Assert.Equal("dbo.tblFoo", name); 33 | } 34 | 35 | private class Product { } 36 | 37 | private class Products { } 38 | 39 | private class Category { } 40 | 41 | [Table("tblFoo")] 42 | private class Foo { } 43 | 44 | [Table("tblFoo", Schema = "dbo")] 45 | private class FooWithSchema { } 46 | } 47 | -------------------------------------------------------------------------------- /test/Dommel.Tests/Dommel.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net9.0 4 | false 5 | 6 | 7 | 8 | runtime; build; native; contentfiles; analyzers 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/Dommel.Tests/DummySqlBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Dommel.Tests; 4 | 5 | internal class DummySqlBuilder : ISqlBuilder 6 | { 7 | /// 8 | public string PrefixParameter(string paramName) => $"#{paramName}"; 9 | 10 | /// 11 | public string QuoteIdentifier(string identifier) => identifier; 12 | 13 | /// 14 | public string BuildInsert(Type type, string tableName, string[] columnNames, string[] paramNames) => 15 | $"insert into {tableName} ({string.Join(", ", columnNames)}) values ({string.Join(", ", paramNames)}); select last_insert_rowid() id"; 16 | 17 | /// 18 | public string BuildPaging(string? orderBy, int pageNumber, int pageSize) 19 | { 20 | var start = pageNumber >= 1 ? (pageNumber - 1) * pageSize : 0; 21 | return $" {orderBy} LIMIT {start}, {pageSize}"; 22 | } 23 | 24 | /// 25 | public string LimitClause(int count) => $"limit {count}"; 26 | 27 | /// 28 | public string LikeExpression(string columnName, string parameterName) => $"{columnName} like {parameterName}"; 29 | } 30 | -------------------------------------------------------------------------------- /test/Dommel.Tests/Models.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace Dommel.Tests; 7 | 8 | public class Product 9 | { 10 | public int Id { get; set; } 11 | 12 | [Column("FullName")] 13 | public string? Name { get; set; } 14 | 15 | // One Product has one Category 16 | public Category? Category { get; set; } 17 | 18 | // Represents the foreign key to the category table 19 | public int CategoryId { get; set; } 20 | 21 | // One Product has many Options 22 | public List? Options { get; set; } 23 | } 24 | 25 | public class Category 26 | { 27 | public int Id { get; set; } 28 | 29 | public string? Name { get; set; } 30 | } 31 | 32 | public class ProductOption 33 | { 34 | public int Id { get; set; } 35 | 36 | // One ProductOption has one Product (no navigation) 37 | // Represents the foreign key to the product table 38 | public int ProductId { get; set; } 39 | } 40 | 41 | public class ProductDto 42 | { 43 | // One Product has One 44 | [ForeignKey("CategoryId")] 45 | public CategoryDto? Category { get; set; } 46 | 47 | public int CategoryId { get; set; } 48 | 49 | // One Product has many Options 50 | [ForeignKey("ProductId")] 51 | public List? Options { get; set; } 52 | } 53 | 54 | public class CategoryDto 55 | { 56 | public int Id { get; set; } 57 | 58 | public string? Name { get; set; } 59 | } 60 | 61 | public class ProductOptionDto 62 | { 63 | public int Id { get; set; } 64 | 65 | // One ProductOption has one Product (no navigation) 66 | public int ProductId { get; set; } 67 | } 68 | 69 | public class Order 70 | { 71 | public int Id { get; set; } 72 | 73 | public string? Reference { get; set; } 74 | 75 | public List? OrderLines { get; set; } 76 | 77 | public List? Shipments { get; set; } 78 | 79 | public List? Logs { get; set; } 80 | 81 | public int CustomerId { get; set; } 82 | 83 | public Customer? Customer { get; set; } 84 | 85 | public int EmployeeId { get; set; } 86 | 87 | public Employee? Employee { get; set; } 88 | 89 | public int PricingSettingsId { get; set; } 90 | 91 | public PricingSettings? PricingSettings { get; set; } 92 | } 93 | 94 | public class OrderLine : IEquatable 95 | { 96 | public OrderLine() 97 | { 98 | } 99 | 100 | public OrderLine(int id, string line) 101 | { 102 | Id = id; 103 | Line = line; 104 | } 105 | 106 | public int Id { get; set; } 107 | 108 | public int OrderId { get; set; } 109 | 110 | public string? Line { get; set; } 111 | 112 | public bool Equals(OrderLine? other) => Id == other?.Id; 113 | } 114 | 115 | public class Customer 116 | { 117 | public int Id { get; set; } 118 | 119 | public string? Name { get; set; } 120 | } 121 | 122 | public class Employee 123 | { 124 | public int Id { get; set; } 125 | 126 | public string? Name { get; set; } 127 | } 128 | 129 | public class OrderLog : IEquatable 130 | { 131 | public OrderLog() 132 | { 133 | } 134 | 135 | public OrderLog(int id, string message) 136 | { 137 | Id = id; 138 | Message = message; 139 | } 140 | 141 | public int Id { get; set; } 142 | 143 | public int OrderId { get; set; } 144 | 145 | public string? Message { get; set; } 146 | 147 | public bool Equals(OrderLog? other) => Id == other?.Id; 148 | } 149 | 150 | public class PricingSettings 151 | { 152 | public int Id { get; set; } 153 | 154 | public decimal VatPercentage { get; set; } = 21.0M; 155 | } 156 | 157 | public class Shipment : IEquatable 158 | { 159 | public Shipment() 160 | { 161 | } 162 | 163 | public Shipment(int id, string location) 164 | { 165 | Id = id; 166 | Location = location; 167 | } 168 | 169 | public int Id { get; set; } 170 | 171 | public int OrderId { get; set; } 172 | 173 | public string? Location { get; set; } 174 | 175 | public bool Equals(Shipment? other) => Id == other?.Id; 176 | } -------------------------------------------------------------------------------- /test/Dommel.Tests/MultiMapTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using static Dommel.DommelMapper; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class MultiMapTests 7 | { 8 | private readonly MySqlSqlBuilder _sqlBuilder = new(); 9 | 10 | [Fact] 11 | public void BuildMultiMapQuery_OneToOne() 12 | { 13 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(Category) }, null, out var parameters); 14 | var expectedQuery = "select * from `Products` left join `Categories` on `Products`.`CategoryId` = `Categories`.`Id`"; 15 | Assert.Equal(expectedQuery, query); 16 | Assert.Null(parameters); 17 | } 18 | 19 | [Fact] 20 | public void BuildMultiMapQuery_OneToOneSingle() 21 | { 22 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(Category) }, 1, out var parameters); 23 | var expectedQuery = "select * from `Products` left join `Categories` on `Products`.`CategoryId` = `Categories`.`Id` where `Products`.`Id` = @Id"; 24 | Assert.Equal(expectedQuery, query); 25 | Assert.Equal("Id", Assert.Single(parameters?.ParameterNames!)); 26 | } 27 | 28 | [Fact] 29 | public void BuildMultiMapQuery_OneToMany() 30 | { 31 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(ProductOption) }, null, out var parameters); 32 | var expectedQuery = "select * from `Products` left join `ProductOptions` on `Products`.`Id` = `ProductOptions`.`ProductId`"; 33 | Assert.Equal(expectedQuery, query); 34 | Assert.Null(parameters); 35 | } 36 | 37 | [Fact] 38 | public void BuildMultiMapQuery_OneToManySingle() 39 | { 40 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(ProductOption) }, 1, out var parameters); 41 | var expectedQuery = "select * from `Products` left join `ProductOptions` on `Products`.`Id` = `ProductOptions`.`ProductId` where `Products`.`Id` = @Id"; 42 | Assert.Equal(expectedQuery, query); 43 | Assert.Equal("Id", Assert.Single(parameters?.ParameterNames!)); 44 | } 45 | 46 | [Fact] 47 | public void BuildMultiMapQuery_OneToOneOneToMany() 48 | { 49 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(Category), typeof(ProductOption) }, null, out var parameters); 50 | var expectedQuery = "select * from `Products` " + 51 | "left join `Categories` on `Products`.`CategoryId` = `Categories`.`Id` " + 52 | "left join `ProductOptions` on `Products`.`Id` = `ProductOptions`.`ProductId`"; 53 | Assert.Equal(expectedQuery, query); 54 | Assert.Null(parameters); 55 | } 56 | 57 | [Fact] 58 | public void BuildMultiMapQuery_BuildMultiMapQuery_OneToOneOneToManySingle() 59 | { 60 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(Category), typeof(ProductOption) }, 1, out var parameters); 61 | var expectedQuery = "select * from `Products` " + 62 | "left join `Categories` on `Products`.`CategoryId` = `Categories`.`Id` " + 63 | "left join `ProductOptions` on `Products`.`Id` = `ProductOptions`.`ProductId` " + 64 | "where `Products`.`Id` = @Id"; 65 | Assert.Equal(expectedQuery, query); 66 | Assert.Equal("Id", Assert.Single(parameters?.ParameterNames!)); 67 | } 68 | 69 | [Fact] 70 | public void BuildMultiMapQuery_OneToManyOneToOne() 71 | { 72 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(ProductOption), typeof(Category) }, null, out var parameters); 73 | var expectedQuery = "select * from `Products` " + 74 | "left join `ProductOptions` on `Products`.`Id` = `ProductOptions`.`ProductId` " + 75 | "left join `Categories` on `Products`.`CategoryId` = `Categories`.`Id`"; 76 | Assert.Equal(expectedQuery, query); 77 | Assert.Null(parameters); 78 | } 79 | 80 | [Fact] 81 | public void BuildMultiMapQuery_BuildMultiMapQuery_OneToManyOneToOneSingle() 82 | { 83 | var query = BuildMultiMapQuery(_sqlBuilder, typeof(Product), new[] { typeof(Product), typeof(ProductOption), typeof(Category) }, 1, out var parameters); 84 | var expectedQuery = "select * from `Products` " + 85 | "left join `ProductOptions` on `Products`.`Id` = `ProductOptions`.`ProductId` " + 86 | "left join `Categories` on `Products`.`CategoryId` = `Categories`.`Id` " + 87 | "where `Products`.`Id` = @Id"; 88 | Assert.Equal(expectedQuery, query); 89 | Assert.Equal("Id", Assert.Single(parameters?.ParameterNames!)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/Dommel.Tests/MySqlSqlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class MySqlSqlBuilderTests 6 | { 7 | private readonly MySqlSqlBuilder _builder = new(); 8 | 9 | [Fact] 10 | public void BuildInsert() 11 | { 12 | var sql = _builder.BuildInsert(typeof(Product), "Foos", new[] { "Name", "Bar" }, new[] { "@Name", "@Bar" }); 13 | Assert.Equal("insert into Foos (Name, Bar) values (@Name, @Bar); select LAST_INSERT_ID() id", sql); 14 | } 15 | 16 | [Theory] 17 | [InlineData(1, 0)] 18 | [InlineData(2, 15)] 19 | [InlineData(3, 30)] 20 | public void BuildPaging(int pageNumber, int start) 21 | { 22 | var sql = _builder.BuildPaging("asc", pageNumber, 15); 23 | Assert.Equal($" asc limit {start}, 15", sql); 24 | } 25 | 26 | [Fact] 27 | public void PrefixParameter() => Assert.Equal("@Foo", _builder.PrefixParameter("Foo")); 28 | 29 | [Fact] 30 | public void QuoteIdentifier() => Assert.Equal("`Foo`", _builder.QuoteIdentifier("Foo")); 31 | 32 | [Theory] 33 | [InlineData(1)] 34 | [InlineData(5)] 35 | public void LimitClause(int count) 36 | { 37 | var sql = _builder.LimitClause(count); 38 | Assert.Equal($"limit {count}", sql); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Dommel.Tests/ParameterPrefixTest.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using static Dommel.DommelMapper; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class ParameterPrefixTest 7 | { 8 | private static readonly ISqlBuilder SqlBuilder = new DummySqlBuilder(); 9 | 10 | [Fact] 11 | public void Get() 12 | { 13 | var sql = BuildGetById(SqlBuilder, typeof(Foo), new[] { (object)1 }, out var parameters); 14 | Assert.Equal("select * from Foos where Foos.Id = #Id", sql); 15 | Assert.Single(parameters.ParameterNames); 16 | } 17 | 18 | [Fact] 19 | public void Select() 20 | { 21 | // Arrange 22 | var builder = new DummySqlBuilder(); 23 | var sqlExpression = new SqlExpression(builder); 24 | 25 | // Act 26 | var sql = sqlExpression.Where(p => p.Id == 1).ToSql(out var dynamicParameters); 27 | 28 | // Assert 29 | Assert.Equal("where Foos.Id = #p1", sql.Trim()); 30 | Assert.Single(dynamicParameters.ParameterNames); 31 | } 32 | 33 | [Fact] 34 | public void TestInsert() 35 | { 36 | var sql = BuildInsertQuery(SqlBuilder, typeof(Foo)); 37 | Assert.Equal("insert into Foos (Bar) values (#Bar); select last_insert_rowid() id", sql); 38 | } 39 | 40 | [Fact] 41 | public void TestUpdate() 42 | { 43 | var sql = BuildUpdateQuery(SqlBuilder, typeof(Foo)); 44 | Assert.Equal("update Foos set Bar = #Bar where Id = #Id", sql); 45 | } 46 | 47 | [Fact] 48 | public void TestDelete() 49 | { 50 | var sql = BuildDeleteQuery(SqlBuilder, typeof(Foo)); 51 | Assert.Equal("delete from Foos where Id = #Id", sql); 52 | } 53 | 54 | public class Foo 55 | { 56 | public int Id { get; set; } 57 | 58 | public string? Bar { get; set; } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/Dommel.Tests/PostgresSqlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using Xunit; 5 | 6 | namespace Dommel.Tests; 7 | 8 | public class PostgresSqlBuilderTests 9 | { 10 | private readonly PostgresSqlBuilder _builder = new(); 11 | 12 | [Fact] 13 | public void BuildInsert() 14 | { 15 | var sql = _builder.BuildInsert(typeof(Product), "Foos", new[] { "Name", "Bar" }, new[] { "@Name", "@Bar" }); 16 | Assert.Equal("insert into Foos (Name, Bar) values (@Name, @Bar) returning (\"Id\")", sql); 17 | } 18 | 19 | [Fact] 20 | public void BuildInsert_Returning2() 21 | { 22 | var sql = _builder.BuildInsert(typeof(Foo), "Foos", new[] { "Name", "Bar" }, new[] { "@Name", "@Bar" }); 23 | Assert.Equal("insert into Foos (Name, Bar) values (@Name, @Bar) returning (\"Id1\", \"Id2\")", sql); 24 | } 25 | 26 | private class Foo 27 | { 28 | [Key] 29 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 30 | public Guid Id1 { get; set; } 31 | 32 | [Key] 33 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 34 | public Guid Id2 { get; set; } 35 | } 36 | 37 | [Theory] 38 | [InlineData(1, 0)] 39 | [InlineData(2, 15)] 40 | [InlineData(3, 30)] 41 | public void BuildPaging(int pageNumber, int start) 42 | { 43 | var sql = _builder.BuildPaging("asc", pageNumber, 15); 44 | Assert.Equal($" asc offset {start} limit 15", sql); 45 | } 46 | 47 | [Fact] 48 | public void PrefixParameter() => Assert.Equal("@Foo", _builder.PrefixParameter("Foo")); 49 | 50 | [Fact] 51 | public void QuoteIdentifier() => Assert.Equal("\"Foo\"", _builder.QuoteIdentifier("Foo")); 52 | 53 | [Fact] 54 | public void BuildInsert_ThrowsWhenTypeIsNull() 55 | { 56 | var builder = new PostgresSqlBuilder(); 57 | Assert.Throws("type", () => builder.BuildInsert(null!, null!, null!, null!)); 58 | } 59 | 60 | [Theory] 61 | [InlineData(1)] 62 | [InlineData(5)] 63 | public void LimitClause(int count) 64 | { 65 | var sql = _builder.LimitClause(count); 66 | Assert.Equal($"limit {count}", sql); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/Dommel.Tests/ProjectTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using static Dommel.DommelMapper; 4 | 5 | namespace Dommel.Tests; 6 | 7 | public class ProjectTests 8 | { 9 | private static readonly ISqlBuilder SqlBuilder = new SqlServerSqlBuilder(); 10 | 11 | [Fact] 12 | public void ProjectById() 13 | { 14 | var sql = BuildProjectById(SqlBuilder, typeof(ProjectedFoo), 42, out var parameters); 15 | Assert.Equal("select [Id], [Name], [DateUpdated] from [ProjectedFoos] where [ProjectedFoos].[Id] = @Id", sql); 16 | Assert.NotNull(parameters); 17 | } 18 | 19 | [Fact] 20 | public void ProjectAll() 21 | { 22 | var sql = BuildProjectAllQuery(SqlBuilder, typeof(ProjectedFoo)); 23 | Assert.Equal("select [Id], [Name], [DateUpdated] from [ProjectedFoos]", sql); 24 | } 25 | 26 | [Fact] 27 | public void ProjectPaged() 28 | { 29 | var sql = BuildProjectPagedQuery(SqlBuilder, typeof(ProjectedFoo), 1, 5); 30 | Assert.Equal("select [Id], [Name], [DateUpdated] from [ProjectedFoos] order by [ProjectedFoos].[Id] offset 0 rows fetch next 5 rows only", sql); 31 | } 32 | 33 | public class ProjectedFoo 34 | { 35 | public int Id { get; set; } 36 | 37 | public string? Name { get; set; } 38 | 39 | public DateTime? DateUpdated { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/Dommel.Tests/ResolversTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Xunit; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class ResolversTests 7 | { 8 | private readonly ISqlBuilder _sqlBuilder = new SqlServerSqlBuilder(); 9 | 10 | [Fact] 11 | public void Table_WithSchema() 12 | { 13 | Assert.Equal("[dbo].[Qux]", Resolvers.Table(typeof(FooQux), _sqlBuilder)); 14 | Assert.Equal("[foo].[dbo].[Qux]", Resolvers.Table(typeof(FooDboQux), _sqlBuilder)); 15 | } 16 | 17 | [Fact] 18 | public void Table_NoCacheConflictNestedClass() 19 | { 20 | Assert.Equal("[BarA]", Resolvers.Table(typeof(Foo.Bar), _sqlBuilder)); 21 | Assert.Equal("[BarB]", Resolvers.Table(typeof(Baz.Bar), _sqlBuilder)); 22 | } 23 | 24 | [Fact] 25 | public void Column_NoCacheConflictNestedClass() 26 | { 27 | Assert.Equal("[BarA].[BazA]", Resolvers.Column(typeof(Foo.Bar).GetProperty("Baz")!, _sqlBuilder)); 28 | Assert.Equal("[BarB].[BazB]", Resolvers.Column(typeof(Baz.Bar).GetProperty("Baz")!, _sqlBuilder)); 29 | } 30 | 31 | [Fact] 32 | public void ForeignKey_NoCacheConflictNestedClass() 33 | { 34 | var foreignKeyA = Resolvers.ForeignKeyProperty(typeof(Foo.BarChild), typeof(Foo.Bar), out _); 35 | var foreignKeyB = Resolvers.ForeignKeyProperty(typeof(Baz.BarChild), typeof(Baz.Bar), out _); 36 | 37 | Assert.Equal(typeof(Foo.BarChild).GetProperty("BarId"), foreignKeyA); 38 | Assert.Equal(typeof(Baz.BarChild).GetProperty("BarId"), foreignKeyB); 39 | } 40 | 41 | [Fact] 42 | public void KeyProperty() 43 | { 44 | var key = Assert.Single(Resolvers.KeyProperties(typeof(Product))); 45 | Assert.Equal(typeof(Product).GetProperty("Id"), key.Property); 46 | } 47 | 48 | public class Foo 49 | { 50 | [Table("BarA")] 51 | public class Bar 52 | { 53 | [Column("BazA")] 54 | public string? Baz { get; set; } 55 | } 56 | 57 | [Table("BarA")] 58 | public class BarChild 59 | { 60 | public int BarId { get; set; } 61 | } 62 | } 63 | 64 | public class Baz 65 | { 66 | [Table("BarB")] 67 | public class Bar 68 | { 69 | [Column("BazB")] 70 | public string? Baz { get; set; } 71 | } 72 | 73 | [Table("BarA")] 74 | public class BarChild 75 | { 76 | public int BarId { get; set; } 77 | } 78 | } 79 | 80 | [Table("Qux", Schema = "foo.dbo")] 81 | public class FooDboQux 82 | { 83 | public int Id { get; set; } 84 | } 85 | 86 | [Table("Qux", Schema = "dbo")] 87 | public class FooQux 88 | { 89 | public int Id { get; set; } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlExpressions/BooleanExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class BooleanExpressionTests 6 | { 7 | [Fact] 8 | public void Single() 9 | { 10 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 11 | .Where(f => f.Baz) 12 | .ToSql(); 13 | Assert.Equal(" where [Foos].[Baz] = '1'", sql); 14 | } 15 | 16 | [Fact] 17 | public void SingleNot() 18 | { 19 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 20 | .Where(f => !f.Baz) 21 | .ToSql(); 22 | Assert.Equal(" where not ([Foos].[Baz] = '1')", sql); 23 | } 24 | 25 | [Fact] 26 | public void SingleExplicitTrue() 27 | { 28 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 29 | .Where(f => f.Baz == true) 30 | .ToSql(); 31 | Assert.Equal(" where [Foos].[Baz] = @p1", sql); 32 | } 33 | 34 | [Fact] 35 | public void Or() 36 | { 37 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 38 | .Where(f => f.Bar == "A" || f.Bar == "B" || f.Baz || f.Qux) 39 | .ToSql(); 40 | Assert.Equal(" where [Foos].[Bar] = @p1 or [Foos].[Bar] = @p2 or [Foos].[Baz] = '1' or [Foos].[Qux] = '1'", sql); 41 | } 42 | 43 | [Fact] 44 | public void OrWithNot() 45 | { 46 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 47 | .Where(f => f.Baz || !f.Qux) 48 | .ToSql(); 49 | Assert.Equal(" where [Foos].[Baz] = '1' or not ([Foos].[Qux] = '1')", sql); 50 | } 51 | 52 | [Fact] 53 | public void And() 54 | { 55 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 56 | .Where(f => f.Baz && f.Qux) 57 | .ToSql(); 58 | Assert.Equal(" where [Foos].[Baz] = '1' and [Foos].[Qux] = '1'", sql); 59 | } 60 | 61 | [Fact] 62 | public void AndWithNot() 63 | { 64 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 65 | .Where(f => !f.Baz && f.Qux) 66 | .ToSql(); 67 | Assert.Equal(" where not ([Foos].[Baz] = '1') and [Foos].[Qux] = '1'", sql); 68 | } 69 | 70 | [Fact] 71 | public void Combined() 72 | { 73 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 74 | .Where(f => f.Bar == "test" || f.Baz) 75 | .ToSql(); 76 | Assert.Equal(" where [Foos].[Bar] = @p1 or [Foos].[Baz] = '1'", sql); 77 | } 78 | 79 | public class Foo 80 | { 81 | public string? Bar { get; set; } 82 | 83 | public bool Baz { get; set; } 84 | 85 | public bool Qux { get; set; } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlExpressions/LikeTests.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | using Xunit; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class LikeTests 7 | { 8 | [Fact] 9 | public void LikeOperandContains() 10 | { 11 | // Arrange 12 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 13 | 14 | // Act 15 | var expression = sqlExpression.Where(p => p.Bar.Contains("test")); 16 | var sql = expression.ToSql(out var dynamicParameters); 17 | 18 | // Assert 19 | Assert.Equal("where [tblFoo].[Bar] like @p1", sql.Trim()); 20 | Assert.Single(dynamicParameters.ParameterNames); 21 | Assert.Equal("%test%", dynamicParameters.Get("p1")); 22 | } 23 | 24 | [Fact] 25 | public void LikeOperandContainsVariable() 26 | { 27 | // Arrange 28 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 29 | var substring = "test"; 30 | 31 | // Act 32 | var expression = sqlExpression.Where(p => p.Bar.Contains(substring)); 33 | var sql = expression.ToSql(out var dynamicParameters); 34 | 35 | // Assert 36 | Assert.Equal("where [tblFoo].[Bar] like @p1", sql.Trim()); 37 | Assert.Single(dynamicParameters.ParameterNames); 38 | Assert.Equal("%test%", dynamicParameters.Get("p1")); 39 | } 40 | 41 | [Fact] 42 | public void LikeOperandStartsWith() 43 | { 44 | // Arrange 45 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 46 | 47 | // Act 48 | var expression = sqlExpression.Where(p => p.Bar.StartsWith("test")); 49 | var sql = expression.ToSql(out var dynamicParameters); 50 | 51 | // Assert 52 | Assert.Equal("where [tblFoo].[Bar] like @p1", sql.Trim()); 53 | Assert.Single(dynamicParameters.ParameterNames); 54 | Assert.Equal("test%", dynamicParameters.Get("p1")); 55 | } 56 | 57 | [Fact] 58 | public void LikeOperandEndsWith() 59 | { 60 | // Arrange 61 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 62 | 63 | // Act 64 | var expression = sqlExpression.Where(p => p.Bar.EndsWith("test")); 65 | var sql = expression.ToSql(out var dynamicParameters); 66 | 67 | // Assert 68 | Assert.Equal("where [tblFoo].[Bar] like @p1", sql.Trim()); 69 | Assert.Single(dynamicParameters.ParameterNames); 70 | Assert.Equal("%test", dynamicParameters.Get("p1")); 71 | } 72 | 73 | [Table("tblFoo")] 74 | public class Foo 75 | { 76 | public int Id { get; set; } 77 | 78 | public string Bar { get; set; } = ""; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlExpressions/NullExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class NullExpressionTests 6 | { 7 | [Fact] 8 | public void GeneratesCorrectIsEqualSyntax() 9 | { 10 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 11 | .Where(f => f.Bar == null) 12 | .ToSql(); 13 | Assert.Equal(" where [Foos].[Bar] is null", sql); 14 | } 15 | 16 | [Fact] 17 | public void GeneratesCorrectIsNotEqualSyntax() 18 | { 19 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 20 | .Where(f => f.Bar != null) 21 | .ToSql(); 22 | Assert.Equal(" where [Foos].[Bar] is not null", sql); 23 | } 24 | 25 | [Fact] 26 | public void GeneratesInvalidIsEqualSyntaxForInvalidExpression() 27 | { 28 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 29 | .Where(f => null == f.Bar) 30 | .ToSql(); 31 | Assert.Equal(" where = @p1", sql); 32 | } 33 | 34 | [Fact] 35 | public void GeneratesInvalidIsNotEqualSyntaxForInvalidExpression() 36 | { 37 | var sql = new SqlExpression(new SqlServerSqlBuilder()) 38 | .Where(f => null != f.Bar) 39 | .ToSql(); 40 | Assert.Equal(" where <> @p1", sql); 41 | } 42 | 43 | public class Foo 44 | { 45 | public string? Bar { get; set; } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlExpressions/PageTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class PageTests 6 | { 7 | private readonly SqlExpression _sqlExpression = new(new SqlServerSqlBuilder()); 8 | 9 | [Fact] 10 | public void GeneratesSql() 11 | { 12 | var sql = _sqlExpression.Page(1, 5).ToSql(); 13 | Assert.Equal(" order by [Products].[Id] asc offset 0 rows fetch next 5 rows only", sql); 14 | } 15 | 16 | [Fact] 17 | public void Page_OrderBy() 18 | { 19 | var sql = _sqlExpression.Page(1, 5).OrderBy(p => p.CategoryId).ToSql(); 20 | Assert.Equal(" order by [Products].[CategoryId] asc offset 0 rows fetch next 5 rows only", sql); 21 | } 22 | 23 | [Fact] 24 | public void OrderBy_Page_OrderBy() 25 | { 26 | var sql = _sqlExpression.OrderBy(p => p.Name).Page(1, 5).OrderByDescending(p => p.CategoryId).ToSql(); 27 | Assert.Equal(" order by [Products].[FullName] asc, [Products].[CategoryId] desc offset 0 rows fetch next 5 rows only", sql); 28 | } 29 | } -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlExpressions/SelectExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class SelectExpressionTests 7 | { 8 | private readonly SqlExpression _sqlExpression = new(new SqlServerSqlBuilder()); 9 | 10 | [Fact] 11 | public void Select_AllProperties() 12 | { 13 | var sql = _sqlExpression 14 | .Select() 15 | .ToSql(); 16 | Assert.Equal("select * from [Products]", sql); 17 | } 18 | 19 | [Fact] 20 | public void Select_ThrowsForNullSelector() => Assert.Throws("selector", () => _sqlExpression.Select(null!)); 21 | 22 | [Fact] 23 | public void Select_ThrowsForEmptyProjection() 24 | { 25 | var ex = Assert.Throws("selector", () => _sqlExpression.Select(x => new object())); 26 | Assert.Equal(new ArgumentException("Projection over type 'Product' yielded no properties.", "selector").Message, ex.Message); 27 | } 28 | 29 | [Fact] 30 | public void Select_SingleProperty() 31 | { 32 | var sql = _sqlExpression 33 | .Select(p => new { p.Id }) 34 | .ToSql(); 35 | Assert.Equal("select [Products].[Id] from [Products]", sql); 36 | } 37 | 38 | [Fact] 39 | public void Select_MultipleProperties() 40 | { 41 | var sql = _sqlExpression 42 | .Select(p => new { p.Id, p.Name }) 43 | .ToSql(); 44 | Assert.Equal("select [Products].[Id], [Products].[FullName] from [Products]", sql); 45 | } 46 | } -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlExpressions/SqlExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Xunit; 4 | 5 | namespace Dommel.Tests; 6 | 7 | public class SqlExpressionTests 8 | { 9 | [Fact] 10 | public void ToString_ReturnsSql() 11 | { 12 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 13 | var sql = sqlExpression.Where(p => p.Name == "Chai").ToSql(); 14 | Assert.Equal(" where [Products].[FullName] = @p1", sql); 15 | Assert.Equal(sql, sqlExpression.ToString()); 16 | } 17 | 18 | [Fact] 19 | public void ToStringVisitation_ReturnsSql() 20 | { 21 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 22 | var sql = sqlExpression.Where(p => p.CategoryId.ToString() == "1").ToSql(); 23 | Assert.Equal(" where CAST([Products].[CategoryId] AS CHAR) = @p1", sql); 24 | Assert.Equal(sql, sqlExpression.ToString()); 25 | } 26 | 27 | [Fact] 28 | public void ToString_ThrowsWhenCalledWithArgument() 29 | { 30 | var sqlExpression = new SqlExpression(new SqlServerSqlBuilder()); 31 | var ex = Assert.Throws(() => sqlExpression.Where(p => p.CategoryId.ToString("n2") == "1")); 32 | Assert.Contains("ToString-expression should not contain any argument.", ex.Message); 33 | } 34 | } -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlServerCeSqlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class SqlServerCeSqlBuilderTests 6 | { 7 | private readonly SqlServerCeSqlBuilder _builder = new(); 8 | 9 | [Fact] 10 | public void BuildInsert() 11 | { 12 | var sql = _builder.BuildInsert(typeof(Product), "Foos", new[] { "Name", "Bar" }, new[] { "@Name", "@Bar" }); 13 | Assert.Equal("insert into Foos (Name, Bar) values (@Name, @Bar); select @@IDENTITY", sql); 14 | } 15 | 16 | [Theory] 17 | [InlineData(1, 0)] 18 | [InlineData(2, 15)] 19 | [InlineData(3, 30)] 20 | public void BuildPaging(int pageNumber, int start) 21 | { 22 | var sql = _builder.BuildPaging("asc", pageNumber, 15); 23 | Assert.Equal($" asc offset {start} rows fetch next 15 rows only", sql); 24 | } 25 | 26 | [Fact] 27 | public void PrefixParameter() => Assert.Equal("@Foo", _builder.PrefixParameter("Foo")); 28 | 29 | [Fact] 30 | public void QuoteIdentifier() => Assert.Equal("[Foo]", _builder.QuoteIdentifier("Foo")); 31 | 32 | [Theory] 33 | [InlineData(1)] 34 | [InlineData(5)] 35 | public void LimitClause(int count) 36 | { 37 | var sql = _builder.LimitClause(count); 38 | Assert.Equal($"order by 1 offset 0 rows fetch next {count} rows only", sql); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Dommel.Tests/SqlServerSqlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class SqlServerSqlBuilderTests 6 | { 7 | private readonly SqlServerSqlBuilder _builder = new(); 8 | 9 | [Fact] 10 | public void BuildInsert() 11 | { 12 | var sql = _builder.BuildInsert(typeof(Product), "Foos", new[] { "Name", "Bar" }, new[] { "@Name", "@Bar" }); 13 | Assert.Equal("set nocount on insert into Foos (Name, Bar) values (@Name, @Bar); select scope_identity()", sql); 14 | } 15 | 16 | [Theory] 17 | [InlineData(1, 0)] 18 | [InlineData(2, 15)] 19 | [InlineData(3, 30)] 20 | public void BuildPaging(int pageNumber, int start) 21 | { 22 | var sql = _builder.BuildPaging("asc", pageNumber, 15); 23 | Assert.Equal($" asc offset {start} rows fetch next 15 rows only", sql); 24 | } 25 | 26 | [Fact] 27 | public void PrefixParameter() => Assert.Equal("@Foo", _builder.PrefixParameter("Foo")); 28 | 29 | [Fact] 30 | public void QuoteIdentifier() => Assert.Equal("[Foo]", _builder.QuoteIdentifier("Foo")); 31 | 32 | [Theory] 33 | [InlineData(1)] 34 | [InlineData(5)] 35 | public void LimitClause(int count) 36 | { 37 | var sql = _builder.LimitClause(count); 38 | Assert.Equal($"order by 1 offset 0 rows fetch next {count} rows only", sql); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /test/Dommel.Tests/SqliteSqlBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Dommel.Tests; 4 | 5 | public class SqliteSqlBuilderTests 6 | { 7 | private readonly SqliteSqlBuilder _builder = new(); 8 | 9 | [Fact] 10 | public void BuildInsert() 11 | { 12 | var sql = _builder.BuildInsert(typeof(Product), "Foos", new[] { "Name", "Bar" }, new[] { "@Name", "@Bar" }); 13 | Assert.Equal("insert into Foos (Name, Bar) values (@Name, @Bar); select last_insert_rowid() id", sql); 14 | } 15 | 16 | [Theory] 17 | [InlineData(1, 0)] 18 | [InlineData(2, 15)] 19 | [InlineData(3, 30)] 20 | public void BuildPaging(int pageNumber, int start) 21 | { 22 | var sql = _builder.BuildPaging("asc", pageNumber, 15); 23 | Assert.Equal($" asc LIMIT {start}, 15", sql); 24 | } 25 | 26 | [Fact] 27 | public void PrefixParameter() => Assert.Equal("@Foo", _builder.PrefixParameter("Foo")); 28 | 29 | [Fact] 30 | public void QuoteIdentifier() => Assert.Equal("Foo", _builder.QuoteIdentifier("Foo")); 31 | 32 | [Theory] 33 | [InlineData(1)] 34 | [InlineData(5)] 35 | public void LimitClause(int count) 36 | { 37 | var sql = _builder.LimitClause(count); 38 | Assert.Equal($"limit {count}", sql); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Dommel.Tests/TypeMapProviderTests.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Xunit; 3 | 4 | namespace Dommel.Tests; 5 | 6 | public class TypeMapProviderTests 7 | { 8 | [Fact] 9 | public void AddsTypeMapProvider() 10 | { 11 | // Arrange 12 | DefaultTypeMap.MatchNamesWithUnderscores = true; 13 | _ = DommelMapper.QueryCache; // Reference anything from DommelMapper to make sure the static ctor runs 14 | 15 | // Act 16 | var typeMap = SqlMapper.TypeMapProvider(typeof(Poco)); 17 | 18 | // Assert 19 | Assert.Equal(typeof(Poco).GetProperty(nameof(Poco.Foo)), typeMap.GetMember("Foo")?.Property); 20 | Assert.Equal(typeof(Poco).GetProperty(nameof(Poco.BarBaz)), typeMap.GetMember("Bar_Baz")?.Property); 21 | } 22 | 23 | private class Poco 24 | { 25 | public int Id { get; set; } 26 | 27 | public string? Foo { get; set; } 28 | 29 | public string? BarBaz { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Dommel.Tests/coverage.cmd: -------------------------------------------------------------------------------- 1 | dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:Include="[Dommel]*" 2 | reportgenerator "-reports:coverage.opencover.xml" "-targetdir:coveragereport" 3 | start coveragereport\index.htm 4 | --------------------------------------------------------------------------------