├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── dotnet.yml │ └── merge.yml ├── .gitignore ├── FluentCommand.sln ├── LICENSE ├── README.md ├── coverlet.runsettings ├── docs ├── assets │ └── logo.png ├── docfx.json ├── guide │ ├── cache.md │ ├── configuration.md │ ├── generation.md │ ├── logging.md │ ├── parameter.md │ ├── query.md │ ├── sql.md │ └── toc.yml ├── index.md ├── reference │ ├── .gitignore │ └── index.md ├── template │ └── public │ │ └── main.css └── toc.yml ├── logo.png ├── src ├── Directory.Build.props ├── FluentCommand.Batch │ ├── BatchError.cs │ ├── BatchFactory.cs │ ├── BatchJob.cs │ ├── BatchJobValidator.cs │ ├── BatchJobVisitor.cs │ ├── BatchProcessor.cs │ ├── FieldDefault.cs │ ├── FieldIndex.cs │ ├── FieldMapping.cs │ ├── FieldMatch.cs │ ├── Fluent │ │ ├── BatchBuilder.cs │ │ ├── BatchFieldBuilder.cs │ │ └── BatchMatchBuilder.cs │ ├── FluentCommand.Batch.csproj │ ├── IBatchFactory.cs │ ├── IBatchProcessor.cs │ ├── IBatchTranslator.cs │ ├── IBatchValidator.cs │ ├── Translators │ │ └── TranslatorBase.cs │ └── Validation │ │ ├── BatchValidator.cs │ │ └── DuplicateException.cs ├── FluentCommand.Caching │ ├── DataConfigurationBuilderExtensions.cs │ ├── DistributedDataCache.cs │ ├── FluentCommand.Caching.csproj │ ├── IDistributedCacheSerializer.cs │ └── MessagePackCacheSerializer.cs ├── FluentCommand.Csv │ ├── CsvCommandExtensions.cs │ └── FluentCommand.Csv.csproj ├── FluentCommand.Dapper │ ├── ConcurrencyTokenTypeHandler.cs │ ├── DataCommandExtensions.cs │ ├── FluentCommand.Dapper.csproj │ └── ReaderFactory.cs ├── FluentCommand.EntityFactory │ ├── DataCommandExtensions.cs │ ├── FluentCommand.EntityFactory.csproj │ ├── ReaderFactory.cs │ └── Reflection │ │ └── ReflectionHelper.cs ├── FluentCommand.Generators │ ├── DataReaderFactoryGenerator.cs │ ├── DataReaderFactoryWriter.cs │ ├── DiagnosticDescriptors.cs │ ├── FluentCommand.Generators.csproj │ ├── GenerateAttributeGenerator.cs │ ├── IndentedStringBuilder.cs │ ├── IsExternalInit.cs │ ├── Models │ │ ├── EntityClass.cs │ │ ├── EntityContext.cs │ │ ├── EntityProperty.cs │ │ ├── EquatableArray.cs │ │ └── InitializationMode.cs │ ├── Properties │ │ └── launchSettings.json │ └── TableAttributeGenerator.cs ├── FluentCommand.Json │ ├── ConcurrencyTokenJsonConverter.cs │ ├── FluentCommand.Json.csproj │ └── JsonCommandExtensions.cs ├── FluentCommand.SqlServer │ ├── Bulk │ │ ├── DataBulkCopy.cs │ │ ├── DataBulkCopyExtensions.cs │ │ ├── DataBulkCopyMapping.cs │ │ └── IDataBulkCopy.cs │ ├── DataConfigurationBuilderExtensions.cs │ ├── FluentCommand.SqlServer.csproj │ ├── Import │ │ ├── FieldDefault.cs │ │ ├── FieldDefinition.cs │ │ ├── FieldDefinitionBuilder.cs │ │ ├── FieldMap.cs │ │ ├── IFieldTranslator.cs │ │ ├── IImportProcessor.cs │ │ ├── IImportValidator.cs │ │ ├── ImportData.cs │ │ ├── ImportDefinition.cs │ │ ├── ImportDefinitionBuilder.cs │ │ ├── ImportFactory.cs │ │ ├── ImportFieldMapping.cs │ │ ├── ImportProcessContext.cs │ │ ├── ImportProcessor.cs │ │ ├── ImportResult.cs │ │ └── ImportValidator.cs │ ├── Merge │ │ ├── DataMerge.cs │ │ ├── DataMergeColumn.cs │ │ ├── DataMergeColumnMapping.cs │ │ ├── DataMergeDefinition.cs │ │ ├── DataMergeExtensions.cs │ │ ├── DataMergeGenerator.cs │ │ ├── DataMergeMapping.cs │ │ ├── DataMergeMode.cs │ │ ├── DataMergeOutputColumn.cs │ │ ├── DataMergeOutputRow.cs │ │ ├── DataReaderWrapper.cs │ │ ├── IDataColumnMapping.cs │ │ ├── IDataMerge.cs │ │ └── IDataMergeMapping.cs │ ├── Query │ │ ├── ChangeTableBuilder.cs │ │ ├── ChangeTableBuilderExtensions.cs │ │ ├── TemporalBuilder.cs │ │ └── TemporalBuilderExtensions.cs │ ├── SqlCommandExtensions.cs │ └── SqlTypeMapping.cs └── FluentCommand │ ├── Attributes │ └── GenerateReaderAttribute.cs │ ├── ConcurrencyToken.cs │ ├── DataCallback.cs │ ├── DataCommand.cs │ ├── DataCommandExtensions.cs │ ├── DataConfiguration.cs │ ├── DataConfigurationBuilder.cs │ ├── DataFieldConverterAttribute.cs │ ├── DataMapping.cs │ ├── DataParameter.cs │ ├── DataParameterHandlers.cs │ ├── DataQueryExtensions.cs │ ├── DataQueryFormatter.cs │ ├── DataQueryLogger.cs │ ├── DataReaderExtensions.cs │ ├── DataSession.cs │ ├── DisposableBase.cs │ ├── Extensions │ ├── CollectionExtensions.cs │ ├── ConvertExtensions.cs │ ├── DataRecordExtensions.cs │ ├── DataTableExtensions.cs │ ├── EnumExtensions.cs │ ├── EnumerableExtensions.cs │ ├── StringBuilderExtensions.cs │ ├── StringExtensions.cs │ └── TypeExtensions.cs │ ├── FluentCommand.csproj │ ├── Handlers │ ├── ConcurrencyTokenHandler.cs │ ├── DateOnlyHandler.cs │ └── TimeOnlyHandler.cs │ ├── IDataCache.cs │ ├── IDataCommand.cs │ ├── IDataConfiguration.cs │ ├── IDataFieldConverter.cs │ ├── IDataParameter.cs │ ├── IDataParameterHandler.cs │ ├── IDataQuery.cs │ ├── IDataQueryAsync.cs │ ├── IDataQueryFormatter.cs │ ├── IDataQueryLogger.cs │ ├── IDataSession.cs │ ├── IDataSessionFactory.cs │ ├── Internal │ ├── HashCode.cs │ ├── Internals.cs │ ├── Singleton.cs │ └── StringBuilderCache.cs │ ├── ListDataReader.cs │ ├── Query │ ├── AggregateFunctions.cs │ ├── DeleteBuilder.cs │ ├── DeleteEntityBuilder.cs │ ├── FilterOperators.cs │ ├── Generators │ │ ├── IQueryGenerator.cs │ │ ├── PostgresqlGenerator.cs │ │ ├── QueryExpressions.cs │ │ ├── SqlServerGenerator.cs │ │ └── SqliteGenerator.cs │ ├── IQueryBuilder.cs │ ├── IQueryStatement.cs │ ├── IStatementBuilder.cs │ ├── IWhereEntityBuilder.cs │ ├── InsertBuilder.cs │ ├── InsertEntityBuilder.cs │ ├── JoinBuilder.cs │ ├── JoinEntityBuilder.cs │ ├── JoinTypes.cs │ ├── LogicalBuilder.cs │ ├── LogicalEntityBuilder.cs │ ├── LogicalOperators.cs │ ├── OrderBuilder.cs │ ├── OrderEntityBuilder.cs │ ├── QueryBuilder.cs │ ├── QueryBuilderExtensions.cs │ ├── QueryParameter.cs │ ├── QueryStatement.cs │ ├── SelectBuilder.cs │ ├── SelectEntityBuilder.cs │ ├── SortDirections.cs │ ├── StatementBuilder.cs │ ├── UpdateBuilder.cs │ ├── UpdateEntityBuilder.cs │ ├── WhereBuilder.cs │ └── WhereEntityBuilder.cs │ ├── QueryMultipleResult.cs │ ├── Reflection │ ├── ExpressionFactory.cs │ ├── FieldAccessor.cs │ ├── IMemberAccessor.cs │ ├── IMemberInformation.cs │ ├── IMethodAccessor.cs │ ├── MemberAccessor.cs │ ├── MethodAccessor.cs │ ├── PropertyAccessor.cs │ └── TypeAccessor.cs │ └── ServiceCollectionExtensions.cs └── test ├── Directory.Build.props ├── FluentCommand.Batch.Tests ├── DataBulkCopyTests.cs ├── DataMergeGeneratorTests.cs ├── FluentCommand.Batch.Tests.csproj ├── UserProfile.cs ├── appsettings.appveyor.json └── appsettings.json ├── FluentCommand.Entities ├── Audit.cs ├── Brand.cs ├── DataType.cs ├── FluentCommand.Entities.csproj ├── Internal │ └── IsExternalInit.cs ├── Member.cs ├── Priority.cs ├── Product.cs ├── ReaderGeneration.cs ├── Role.cs ├── Status.cs ├── StatusConstructor.cs ├── StatusReadOnly.cs ├── StatusRecord.cs ├── TableTest.cs ├── Task.cs ├── TaskExtended.cs ├── User.cs ├── UserImport.cs ├── UserLogin.cs └── UserRole.cs ├── FluentCommand.Generators.Tests ├── DataReaderFactoryWriterTests.cs ├── FluentCommand.Generators.Tests.csproj └── Snapshots │ └── DataReaderFactoryWriterTests.Generate.verified.txt ├── FluentCommand.Performance ├── BenchmarkBase.cs ├── DapperBenchmarks.cs ├── FluentCommand.Performance.csproj ├── FluentCommandBenchmarks.cs ├── HandCodedBenchmarks.cs ├── Post.cs ├── Program.cs └── PropertyAccessorBenchmark.cs ├── FluentCommand.PostgreSQL.Tests ├── DataCommandSqlAsyncTests.cs ├── DataCommandSqlTests.cs ├── DataQueryTests.cs ├── DatabaseCollection.cs ├── DatabaseFixture.cs ├── DatabaseInitializer.cs ├── DatabaseTestBase.cs ├── FluentCommand.PostgreSQL.Tests.csproj ├── Scripts │ ├── Script001.Tracker.Schema.sql │ └── Script002.Tracker.Data.sql ├── appsettings.appveyor.json ├── appsettings.github.json └── appsettings.json ├── FluentCommand.SQLite.Tests ├── DataCommandSqlAsyncTests.cs ├── DataCommandSqlTests.cs ├── DataQueryTests.cs ├── DatabaseCollection.cs ├── DatabaseFixture.cs ├── DatabaseInitializer.cs ├── DatabaseTestBase.cs ├── FluentCommand.SQLite.Tests.csproj ├── Scripts │ ├── Script001.Tracker.Schema.sql │ └── Script002.Tracker.Data.sql └── appsettings.json ├── FluentCommand.SqlServer.Tests ├── CsvTests.cs ├── DataBulkCopyTests.cs ├── DataCacheTests.cs ├── DataCommandProcedureTests.cs ├── DataCommandSqlAsyncTests.cs ├── DataCommandSqlTests.cs ├── DataConfigurationTests.cs ├── DataMergeGeneratorTests.cs ├── DataMergeTests.cs ├── DataQueryTests.cs ├── DataSessionTests.cs ├── DatabaseCollection.cs ├── DatabaseFixture.cs ├── DatabaseInitializer.cs ├── DatabaseTestBase.cs ├── FluentCommand.SqlServer.Tests.csproj ├── GeneratorTests.cs ├── ImportProcessorTests.cs ├── JsonTests.cs ├── ReadOnlyIntent.cs ├── Scripts │ ├── Script001.Tracker.Schema.sql │ ├── Script002.Tracker.Data.sql │ └── Script003.Tracker.StoredProcedure.sql ├── Snapshots │ ├── DataMergeGeneratorTests.BuildMergeDataMismatchTests.verified.txt │ ├── DataMergeGeneratorTests.BuildMergeDataOutputTests.verified.txt │ ├── DataMergeGeneratorTests.BuildMergeDataTableTests.verified.txt │ ├── DataMergeGeneratorTests.BuildMergeDataTests.verified.txt │ ├── DataMergeGeneratorTests.BuildMergeDataTypeTests.verified.txt │ ├── DataMergeGeneratorTests.BuildMergeTests.verified.txt │ └── DataMergeGeneratorTests.BuildTableSqlTest.verified.txt ├── appsettings.appveyor.json ├── appsettings.github.json ├── appsettings.json └── appsettings.linux.json └── FluentCommand.Tests ├── FluentCommand.Tests.csproj ├── Internal └── HashCodeTests.cs ├── Models ├── Command.cs ├── Parameter.cs ├── Truck.cs ├── User.cs └── Vehicle.cs └── Query ├── DeleteBuilderTest.cs ├── InsertBuilderTest.cs ├── QueryBuilderTests.cs ├── SelectBuilderTest.cs ├── Snapshots ├── DeleteBuilderTest.DeleteEntityJoin.verified.txt ├── DeleteBuilderTest.DeleteEntityWithComment.verified.txt ├── DeleteBuilderTest.DeleteEntityWithOutput.verified.txt ├── InsertBuilderTest.InsertEntityValueWithOutput.verified.txt ├── QueryBuilderTests.QueryBuilderSelect.verified.txt ├── SelectBuilderTest.SelectColumnsAliasWhereTagBuilder.verified.txt ├── SelectBuilderTest.SelectEntityAliasWhereTagBuilder.verified.txt ├── SelectBuilderTest.SelectEntityChangeTableBuilder.verified.txt ├── SelectBuilderTest.SelectEntityFilterBuilder.verified.txt ├── SelectBuilderTest.SelectEntityJoinBuilder.verified.txt ├── SelectBuilderTest.SelectEntityNestedWhereBuilder.verified.txt ├── SelectBuilderTest.SelectEntityTemporalBuilder.verified.txt ├── SelectBuilderTest.SelectEntityWhereIn.verified.txt ├── SelectBuilderTest.SelectEntityWhereInDouble.verified.txt ├── SelectBuilderTest.SelectEntityWhereInIf.verified.txt ├── SelectBuilderTest.SelectEntityWhereLimitBuilder.verified.txt ├── SelectBuilderTest.SelectEntityWhereTagBuilder.verified.txt ├── UpdateBuilderTest.UpdateEntityValueWithJoin.verified.txt └── UpdateBuilderTest.UpdateEntityValueWithOutput.verified.txt └── UpdateBuilderTest.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | # All Files 6 | [*] 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 4 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | # XML Configuration Files 14 | [*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct,refactorlog,runsettings}] 15 | indent_size = 2 16 | 17 | # JSON Files 18 | [*.{json,json5,webmanifest}] 19 | indent_size = 2 20 | 21 | # Project Files 22 | [*.{csproj,sqlproj}] 23 | indent_size = 2 24 | 25 | # YAML Files 26 | [*.{yml,yaml}] 27 | indent_size = 2 28 | 29 | # Markdown Files 30 | [*.md] 31 | trim_trailing_whitespace = false 32 | 33 | # Web Files 34 | [*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,pcss,svg,vue}] 35 | indent_size = 2 36 | 37 | # Batch Files 38 | [*.{cmd,bat}] 39 | end_of_line = crlf 40 | 41 | # Bash Files 42 | [*.sh] 43 | end_of_line = lf 44 | 45 | [*.{cs,vb}] 46 | dotnet_sort_system_directives_first = true 47 | dotnet_separate_import_directive_groups = true 48 | dotnet_style_namespace_match_folder = true 49 | 50 | [*.cs] 51 | csharp_using_directive_placement = outside_namespace 52 | csharp_style_namespace_declarations = file_scoped:warning -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: loresoft 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "01:00" 8 | timezone: "America/Chicago" 9 | open-pull-requests-limit: 10 10 | 11 | - package-ecosystem: nuget 12 | directory: "/" 13 | schedule: 14 | interval: daily 15 | time: "02:00" 16 | timezone: "America/Chicago" 17 | open-pull-requests-limit: 10 18 | ignore: 19 | - dependency-name: "Microsoft.CodeAnalysis.CSharp" 20 | - dependency-name: FluentAssertions 21 | versions: [">=8.0.0"] 22 | groups: 23 | Azure: 24 | patterns: 25 | - "Azure.*" 26 | - "Microsoft.Azure.*" 27 | - "Microsoft.Extensions.Azure" 28 | AspNetCoreHealthChecks: 29 | patterns: 30 | - "AspNetCore.HealthChecks.*" 31 | AspNetCore: 32 | patterns: 33 | - "Microsoft.AspNetCore.*" 34 | - "Microsoft.Extensions.Features" 35 | MicrosoftExtensions: 36 | patterns: 37 | - "Microsoft.Extensions.*" 38 | EntityFrameworkCore: 39 | patterns: 40 | - "Microsoft.EntityFrameworkCore.*" 41 | OpenTelemetry: 42 | patterns: 43 | - "OpenTelemetry.*" 44 | Serilog: 45 | patterns: 46 | - "Serilog" 47 | - "Serilog.*" 48 | Hangfire: 49 | patterns: 50 | - "Hangfire" 51 | - "Hangfire.*" 52 | Testcontainers: 53 | patterns: 54 | - "Testcontainers.*" 55 | xUnit: 56 | patterns: 57 | - "xunit" 58 | - "xunit.assert" 59 | - "xunit.core" 60 | - "xunit.extensibility.*" 61 | - "xunit.runner.*" 62 | -------------------------------------------------------------------------------- /.github/workflows/merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto-Merge 2 | on: pull_request 3 | 4 | permissions: 5 | contents: write 6 | pull-requests: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: github.actor == 'dependabot[bot]' 12 | 13 | steps: 14 | - name: Dependabot Metadata 15 | id: metadata 16 | uses: dependabot/fetch-metadata@v2 17 | with: 18 | github-token: "${{ secrets.GITHUB_TOKEN }}" 19 | 20 | - name: Dependabot Auto-Merge PRs 21 | run: gh pr merge --auto --merge "$PR_URL" 22 | env: 23 | PR_URL: ${{github.event.pull_request.html_url}} 24 | GH_TOKEN: ${{secrets.GITHUB_TOKEN}} 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LoreSoft 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. 22 | -------------------------------------------------------------------------------- /coverlet.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | lcov 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loresoft/FluentCommand/e8ba79e74888389f87831889a6d983f8736ec7c2/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "src/FluentCommand/bin/Release/net8.0/FluentCommand.dll", 8 | "src/FluentCommand.SqlServer/bin/Release/net8.0/FluentCommand.SqlServer.dll", 9 | "src/FluentCommand.Json/bin/Release/net8.0/FluentCommand.Json.dll" 10 | ], 11 | "src": "../" 12 | } 13 | ], 14 | "dest": "reference" 15 | } 16 | ], 17 | "build": { 18 | "content": [ 19 | { 20 | "files": [ 21 | "reference/**.yml", 22 | "reference/index.md" 23 | ] 24 | }, 25 | { 26 | "files": [ 27 | "guide/**.md", 28 | "guide/**/toc.yml", 29 | "toc.yml", 30 | "*.md" 31 | ] 32 | } 33 | ], 34 | "resource": [ 35 | { 36 | "files": [ 37 | "assets/**" 38 | ] 39 | } 40 | ], 41 | "postProcessors": [ 42 | "ExtractSearchIndex" 43 | ], 44 | "globalMetadata": { 45 | "_appTitle": "FluentCommand", 46 | "_appName": "FluentCommand", 47 | "_appFooter": "Copyright © 2024 LoreSoft", 48 | "_appLogoPath": "assets/logo.png", 49 | "_appFaviconPath": "assets/logo.png", 50 | "_enableSearch": true 51 | }, 52 | "sitemap": { 53 | "baseUrl": "https://loresoft.com/FluentCommand", 54 | "priority": 0.5, 55 | "changefreq": "daily" 56 | }, 57 | "output": "_site", 58 | "template": [ 59 | "default", 60 | "modern", 61 | "template" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docs/guide/cache.md: -------------------------------------------------------------------------------- 1 | # Caching 2 | -------------------------------------------------------------------------------- /docs/guide/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | ## Configuration for SQL Server 4 | 5 | ```csharp 6 | var dataConfiguration = new DataConfiguration( 7 | SqlClientFactory.Instance, 8 | ConnectionString 9 | ); 10 | ``` 11 | 12 | ## Configure data logger 13 | 14 | ```csharp 15 | var dataLogger = new DataQueryLogger(Output.WriteLine); 16 | var dataConfiguration = new DataConfiguration( 17 | SqlClientFactory.Instance, 18 | ConnectionString, 19 | queryLogger: dataLogger 20 | ); 21 | ``` 22 | 23 | ## Register with dependency injection 24 | 25 | ```csharp 26 | services.AddFluentCommand(builder => builder 27 | .UseConnectionString(ConnectionString) 28 | .UseSqlServer() 29 | ); 30 | ``` 31 | 32 | ## Register using a connection name from the appsettings.json 33 | 34 | ```csharp 35 | services.AddFluentCommand(builder => builder 36 | .UseConnectionName("Tracker") 37 | .UseSqlServer() 38 | ); 39 | ``` 40 | 41 | ```json 42 | { 43 | "ConnectionStrings": { 44 | "Tracker": "Data Source=(local);Initial Catalog=TrackerTest;Integrated Security=True;TrustServerCertificate=True;" 45 | } 46 | } 47 | ``` 48 | 49 | ## Register for PostgreSQL 50 | 51 | ```csharp 52 | services.AddFluentCommand(builder => builder 53 | .UseConnectionName("Tracker") 54 | .AddProviderFactory(NpgsqlFactory.Instance) 55 | .AddPostgreSqlGenerator() 56 | ); 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/guide/generation.md: -------------------------------------------------------------------------------- 1 | # Source Generator 2 | 3 | The project supports generating a DbDataReader from a class via an attribute. Add the `TableAttribute` to a class to generate the needed extension methods. 4 | 5 | ```c# 6 | [Table("Status", Schema = "dbo")] 7 | public class Status 8 | { 9 | public int Id { get; set; } 10 | public string Name { get; set; } 11 | public string Description { get; set; } 12 | public int DisplayOrder { get; set; } 13 | public bool IsActive { get; set; } 14 | public DateTimeOffset Created { get; set; } 15 | public string CreatedBy { get; set; } 16 | public DateTimeOffset Updated { get; set; } 17 | public string UpdatedBy { get; set; } 18 | 19 | [ConcurrencyCheck] 20 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 21 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 22 | public ConcurrencyToken RowVersion { get; set; } 23 | 24 | [NotMapped] 25 | public virtual ICollection Tasks { get; set; } = new List(); 26 | } 27 | ``` 28 | 29 | Extension methods are generated to materialize data command to entities 30 | 31 | ```c# 32 | string email = "kara.thrace@battlestar.com"; 33 | string sql = "select * from [User] where EmailAddress = @EmailAddress"; 34 | var session = configuration.CreateSession(); 35 | var user = await session 36 | .Sql(sql) 37 | .Parameter("@EmailAddress", email) 38 | .QuerySingleAsync(); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/guide/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | -------------------------------------------------------------------------------- /docs/guide/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Configuration 2 | href: configuration.md 3 | - name: SQL Builder 4 | href: sql.md 5 | - name: Parameters 6 | href: parameter.md 7 | - name: Queries 8 | href: query.md 9 | - name: Caching 10 | href: cache.md 11 | - name: Logging 12 | href: logging.md 13 | - name: Generation 14 | href: generation.md 15 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # FluentCommand 2 | 3 | Fluent Wrapper for DbCommand. 4 | 5 | [![Build status](https://github.com/loresoft/FluentCommand/workflows/Build/badge.svg)](https://github.com/loresoft/FluentCommand/actions) 6 | 7 | [![Coverage Status](https://coveralls.io/repos/github/loresoft/FluentCommand/badge.svg?branch=master)](https://coveralls.io/github/loresoft/FluentCommand?branch=master) 8 | 9 | | Package | Version | 10 | | :--- | :--- | 11 | | [FluentCommand](https://www.nuget.org/packages/FluentCommand/) | [![FluentCommand](https://img.shields.io/nuget/v/FluentCommand.svg)](https://www.nuget.org/packages/FluentCommand/) | 12 | | [FluentCommand.SqlServer](https://www.nuget.org/packages/FluentCommand.SqlServer/) | [![FluentCommand.SqlServer](https://img.shields.io/nuget/v/FluentCommand.SqlServer.svg)](https://www.nuget.org/packages/FluentCommand.SqlServer/) | 13 | | [FluentCommand.Json](https://www.nuget.org/packages/FluentCommand.Json/) | [![FluentCommand.Json](https://img.shields.io/nuget/v/FluentCommand.Json.svg)](https://www.nuget.org/packages/FluentCommand.Json/) | 14 | 15 | ## Download 16 | 17 | The FluentCommand library is available on nuget.org via package name `FluentCommand`. 18 | 19 | To install FluentCommand, run the following command in the Package Manager Console 20 | 21 | PM> Install-Package FluentCommand 22 | 23 | More information about NuGet package available at 24 | 25 | 26 | ## Features 27 | 28 | - Fluent wrapper over DbConnection and DbCommand 29 | - Callback for parameter return values 30 | - Automatic handling of connection state 31 | - Caching of results 32 | - Automatic creating of entity from DataReader via Dapper 33 | - Create Dynamic objects from DataReader via Dapper 34 | - Handles multiple result sets 35 | - Basic SQL query builder 36 | - Source Generate DataReader 37 | -------------------------------------------------------------------------------- /docs/reference/.gitignore: -------------------------------------------------------------------------------- 1 | *.yml 2 | .manifest 3 | -------------------------------------------------------------------------------- /docs/reference/index.md: -------------------------------------------------------------------------------- 1 | # FluentCommand Reference 2 | 3 | The API Reference Documentation 4 | -------------------------------------------------------------------------------- /docs/template/public/main.css: -------------------------------------------------------------------------------- 1 | #logo { 2 | width: 50px; 3 | height: 50px; 4 | margin-right: 0.5em; 5 | } 6 | 7 | #breadcrumb { 8 | display: none; 9 | } 10 | -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: User Guide 2 | href: guide/ 3 | - name: Reference 4 | href: reference/ 5 | homepage: reference/index.md 6 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loresoft/FluentCommand/e8ba79e74888389f87831889a6d983f8736ec7c2/logo.png -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fluent Wrapper for DbCommand 5 | Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft 6 | LoreSoft 7 | en-US 8 | true 9 | orm;sql;micro-orm;database 10 | https://github.com/loresoft/FluentCommand 11 | MIT 12 | logo.png 13 | README.md 14 | git 15 | https://github.com/loresoft/FluentCommand 16 | true 17 | 18 | 19 | 20 | embedded 21 | true 22 | false 23 | 24 | 25 | 26 | true 27 | 28 | 29 | 30 | en-US 31 | latest 32 | enable 33 | 1591 34 | 35 | 36 | 37 | v 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | true 48 | \ 49 | false 50 | 51 | 52 | true 53 | \ 54 | false 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/FluentCommand.Batch/BatchError.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace FluentCommand.Batch; 4 | 5 | /// 6 | /// How to handle batch errors 7 | /// 8 | public enum BatchError 9 | { 10 | /// 11 | /// Skip the error and move to next row 12 | /// 13 | [Description("Skip error and continue")] 14 | Skip, 15 | /// 16 | /// Quit processing batch 17 | /// 18 | [Description("Stop processing")] 19 | Quit 20 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/BatchFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | using FluentCommand.Batch.Validation; 7 | 8 | namespace FluentCommand.Batch; 9 | 10 | public class BatchFactory : IBatchFactory 11 | { 12 | private readonly List _validators; 13 | private readonly List _translators; 14 | 15 | 16 | public BatchFactory(IEnumerable validators, IEnumerable translators) 17 | { 18 | _validators = validators?.ToList() ?? new List(); 19 | _translators = translators?.ToList() ?? new List(); 20 | } 21 | 22 | /// 23 | /// Resolves the field translator for the specified name. 24 | /// 25 | /// The name of the translator. 26 | /// 27 | /// An instance of if found; otherwise null. 28 | /// 29 | public IBatchTranslator ResolveTranslator(string name) 30 | { 31 | if (string.IsNullOrWhiteSpace(name)) 32 | return null; 33 | 34 | return _translators 35 | .FirstOrDefault(t => t.GetType().Name == name); 36 | } 37 | 38 | /// 39 | /// Resolves the row validator for the specified name. 40 | /// 41 | /// The name of the validator. 42 | /// 43 | /// An instance of if found; otherwise null. 44 | /// 45 | public IBatchValidator ResolveValidator(string name) 46 | { 47 | if (string.IsNullOrWhiteSpace(name)) 48 | return null; 49 | 50 | return _validators 51 | .FirstOrDefault(t => t.GetType().Name == name); 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/BatchJobValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Linq; 4 | 5 | namespace FluentCommand.Batch; 6 | 7 | /// 8 | /// A class to validate an instance of 9 | /// 10 | public class BatchJobValidator : BatchJobVisitor 11 | { 12 | /// 13 | /// Validates the specified . 14 | /// 15 | /// The to validate. 16 | /// 17 | public virtual bool Validate(BatchJob batchJob) 18 | { 19 | Visit(batchJob); 20 | 21 | return true; 22 | } 23 | 24 | /// 25 | /// Visits the specified . 26 | /// 27 | /// The to visit. 28 | /// 29 | /// Missing key column. Please select a column to be the key. 30 | /// or 31 | /// Missing column selection. Please select a column to be included. 32 | /// 33 | public override void Visit(BatchJob batchJob) 34 | { 35 | // must have key 36 | var keyColumn = batchJob.Fields.FirstOrDefault(m => m.IsKey); 37 | if (keyColumn == null) 38 | throw new ValidationException("Missing key field. Please select a field to be the key."); 39 | 40 | // must have column 41 | bool hasColumn = batchJob.Fields.Any(m => (m.Index.HasValue || m.Default.HasValue) && !m.IsKey); 42 | if (!hasColumn) 43 | throw new ValidationException("Missing field selection. Please select a field to be included."); 44 | 45 | base.Visit(batchJob); 46 | } 47 | 48 | /// 49 | /// Visits the specified . 50 | /// 51 | /// The to visit. 52 | /// Missing source column mapping. Please select a source column. 53 | public override void VisitFieldMapping(FieldMapping fieldMapping) 54 | { 55 | var c = fieldMapping; 56 | 57 | if (c.Required && (c.Index == null || c.Index == -1)) 58 | throw new ValidationException("Missing required field mapping. Please select a source field for '" + c.DisplayName + "'."); 59 | 60 | base.VisitFieldMapping(fieldMapping); 61 | } 62 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/BatchJobVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace FluentCommand.Batch; 6 | 7 | /// 8 | /// A class that represents a visitor for . 9 | /// 10 | public abstract class BatchJobVisitor 11 | { 12 | /// 13 | /// Visits the specified . 14 | /// 15 | /// The to visit. 16 | public virtual void Visit(BatchJob batchJob) 17 | { 18 | foreach (var item in batchJob.Fields) 19 | VisitFieldMapping(item); 20 | 21 | } 22 | 23 | /// 24 | /// Visits the specified . 25 | /// 26 | /// The to visit. 27 | public virtual void VisitFieldMapping(FieldMapping fieldMapping) 28 | { 29 | foreach (var item in fieldMapping.MatchDefinitions) 30 | VisitFieldMatch(item); 31 | 32 | } 33 | 34 | /// 35 | /// Visits the specified . 36 | /// 37 | /// The to visit. 38 | public virtual void VisitFieldMatch(FieldMatch fieldMatch) 39 | { 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /src/FluentCommand.Batch/FieldDefault.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Batch; 4 | 5 | /// 6 | /// Default field value 7 | /// 8 | public enum FieldDefault 9 | { 10 | /// 11 | /// Use the as the default value. 12 | /// 13 | UserName, 14 | /// 15 | /// Use the current date time as the default value. 16 | /// 17 | CurrentDate, 18 | /// 19 | /// Use a static default value. 20 | /// 21 | Static 22 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/FieldIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Batch; 4 | 5 | /// 6 | /// Field definition 7 | /// 8 | public class FieldIndex 9 | { 10 | /// 11 | /// Gets or sets the field index. 12 | /// 13 | /// 14 | /// The field index. 15 | /// 16 | public int? Index { get; set; } 17 | 18 | /// 19 | /// Gets or sets the name of the field. 20 | /// 21 | /// 22 | /// The name of the field. 23 | /// 24 | public string Name { get; set; } 25 | 26 | /// 27 | /// Returns a that represents this instance. 28 | /// 29 | /// 30 | /// A that represents this instance. 31 | /// 32 | public override string ToString() 33 | { 34 | return $"Name: {Name}, Index: {Index}"; 35 | } 36 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/FieldMatch.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace FluentCommand.Batch; 8 | 9 | /// 10 | /// A class to define a field match for field mapping. 11 | /// 12 | public class FieldMatch 13 | { 14 | /// 15 | /// Gets or sets the text used to match a field name. If is true, this will be used as a regular expression. 16 | /// 17 | /// 18 | /// The text used to match a field name. 19 | /// 20 | public string Text { get; set; } 21 | 22 | /// 23 | /// Gets or sets the default translator source when matched. 24 | /// 25 | /// 26 | /// The translator source. 27 | /// 28 | public string TranslatorSource { get; set; } 29 | 30 | /// 31 | /// Gets or sets a value indicating whether to use as regular expression. 32 | /// 33 | /// 34 | /// true to use regex; otherwise, false. 35 | /// 36 | public bool UseRegex { get; set; } 37 | } 38 | -------------------------------------------------------------------------------- /src/FluentCommand.Batch/Fluent/BatchMatchBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Batch.Fluent; 4 | 5 | public class BatchMatchBuilder 6 | { 7 | private readonly FieldMatch _fieldMatch; 8 | 9 | public BatchMatchBuilder(FieldMatch fieldMatch) 10 | { 11 | _fieldMatch = fieldMatch; 12 | } 13 | 14 | public BatchMatchBuilder Text(string value) 15 | { 16 | _fieldMatch.Text = value; 17 | return this; 18 | } 19 | 20 | public BatchMatchBuilder UseRegex(bool value = true) 21 | { 22 | _fieldMatch.UseRegex = value; 23 | return this; 24 | } 25 | 26 | public BatchMatchBuilder TranslatorSource(string value) 27 | { 28 | _fieldMatch.TranslatorSource = value; 29 | return this; 30 | } 31 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/FluentCommand.Batch.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net6.0;net7.0 4 | 5 | 6 | 7 | 1701;1702;1591 8 | latest 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/FluentCommand.Batch/IBatchFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Batch; 4 | 5 | /// 6 | /// An for resolving named type 7 | /// 8 | public interface IBatchFactory 9 | { 10 | /// 11 | /// Resolves the field translator for the specified name. 12 | /// 13 | /// The name of the translator. 14 | /// An instance of if found; otherwise null. 15 | IBatchTranslator ResolveTranslator(string name); 16 | 17 | /// 18 | /// Resolves the row validator for the specified name. 19 | /// 20 | /// The name of the validator. 21 | /// An instance of if found; otherwise null. 22 | IBatchValidator ResolveValidator(string name); 23 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/IBatchProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | 5 | using FluentCommand.Merge; 6 | 7 | namespace FluentCommand.Batch; 8 | 9 | public interface IBatchProcessor 10 | { 11 | IEnumerable Process(BatchJob batchJob); 12 | DataTable CreateTable(BatchJob batchJob); 13 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/IBatchTranslator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Batch; 4 | 5 | public interface IBatchTranslator 6 | { 7 | string[] Sources { get; } 8 | 9 | object Translate(string source, object original); 10 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/IBatchValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | 4 | namespace FluentCommand.Batch; 5 | 6 | public interface IBatchValidator 7 | { 8 | void Reset(); 9 | 10 | void ValidateRow(BatchJob batchJob, DataRow targetRow); 11 | } -------------------------------------------------------------------------------- /src/FluentCommand.Batch/Validation/DuplicateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Batch.Validation; 4 | 5 | public class DuplicateException : Exception 6 | { 7 | public DuplicateException(string message) 8 | : base(message) 9 | { 10 | } 11 | 12 | public DuplicateException(string message, Exception innerException) 13 | : base(message, innerException) 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/FluentCommand.Caching/DataConfigurationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using MessagePack.Resolvers; 3 | 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | 6 | namespace FluentCommand.Caching; 7 | 8 | /// 9 | /// Extension methods for 10 | /// 11 | public static class DataConfigurationBuilderExtensions 12 | { 13 | /// 14 | /// Adds the distributed data cache. 15 | /// 16 | /// The data configuration builder. 17 | /// 18 | public static DataConfigurationBuilder AddDistributedDataCache(this DataConfigurationBuilder builder) 19 | { 20 | builder.AddService(sp => 21 | { 22 | sp.TryAddSingleton(ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.Lz4BlockArray)); 23 | sp.TryAddSingleton(); 24 | }); 25 | 26 | builder.AddDataCache(); 27 | 28 | return builder; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/FluentCommand.Caching/FluentCommand.Caching.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0;net9.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/FluentCommand.Caching/IDistributedCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Caching; 2 | 3 | /// 4 | /// Interface defining a serializer of distrubted cache 5 | /// 6 | public interface IDistributedCacheSerializer 7 | { 8 | /// 9 | /// Serializes the specified instance to a byte array for caching. 10 | /// 11 | /// The type to serialize 12 | /// The instance to serialize. 13 | /// 14 | /// The byte array of the serialized instance 15 | /// 16 | byte[] Serialize(T instance); 17 | 18 | /// 19 | /// Serializes the specified instance to a byte array for caching. 20 | /// 21 | /// The type to serialize 22 | /// The instance to serialize. 23 | /// The cancellation token. 24 | /// 25 | /// The byte array of the serialized instance 26 | /// 27 | Task SerializeAsync(T instance, CancellationToken cancellationToken = default); 28 | 29 | /// 30 | /// Deserializes the specified byte array into an instance of . 31 | /// 32 | /// The type to deserialize 33 | /// The byte array to deserialize. 34 | /// 35 | /// An instance of deserialized 36 | /// 37 | T Deserialize(byte[] byteArray); 38 | 39 | /// 40 | /// Deserializes the specified byte array into an instance of . 41 | /// 42 | /// The type to deserialize 43 | /// The byte array to deserialize. 44 | /// The cancellation token. 45 | /// 46 | /// An instance of deserialized 47 | /// 48 | Task DeserializeAsync(byte[] byteArray, CancellationToken cancellationToken = default); 49 | } 50 | -------------------------------------------------------------------------------- /src/FluentCommand.Csv/FluentCommand.Csv.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0;net9.0 5 | FluentCommand 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/FluentCommand.Dapper/ConcurrencyTokenTypeHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using Dapper; 4 | 5 | namespace FluentCommand; 6 | 7 | public class ConcurrencyTokenTypeHandler : SqlMapper.TypeHandler 8 | { 9 | public override ConcurrencyToken Parse(object value) 10 | { 11 | return value switch 12 | { 13 | string textToken => new ConcurrencyToken(textToken), 14 | byte[] byteToken => new ConcurrencyToken(byteToken), 15 | _ => ConcurrencyToken.None 16 | }; 17 | } 18 | 19 | public override void SetValue(IDbDataParameter parameter, ConcurrencyToken value) 20 | { 21 | parameter.Value = value.Value; 22 | parameter.DbType = DbType.Binary; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/FluentCommand.Dapper/FluentCommand.Dapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net6.0;net7.0 5 | FluentCommand 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/FluentCommand.Dapper/ReaderFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using Dapper; 4 | 5 | using static Dapper.SqlMapper; 6 | 7 | namespace FluentCommand; 8 | 9 | /// 10 | /// A class with data reader factory methods. 11 | /// 12 | public static class ReaderFactory 13 | { 14 | /// 15 | /// A factory for creating TEntity objects from the current row in the . 16 | /// 17 | /// The type of the entity. 18 | /// The open to get the object from. 19 | /// A TEntity object having property names set that match the field names in the . 20 | public static TEntity EntityFactory(IDataReader reader) 21 | where TEntity : class 22 | { 23 | // parser is cached in dapper, ok to repeated calls 24 | var parser = reader.GetRowParser(); 25 | return parser(reader); 26 | } 27 | 28 | /// 29 | /// A factory for creating dynamic objects from the current row in the . 30 | /// 31 | /// The open to get the object from. 32 | /// A dynamic object having property names set that match the field names in the . 33 | public static dynamic DynamicFactory(IDataReader reader) 34 | { 35 | // parser is cached in dapper, ok to repeated calls 36 | var parser = reader.GetRowParser(); 37 | return parser(reader); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/FluentCommand.EntityFactory/FluentCommand.EntityFactory.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net6.0;net7.0 4 | FluentCommand 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/FluentCommand.EntityFactory/ReaderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Dynamic; 5 | 6 | using FluentCommand.Reflection; 7 | 8 | namespace FluentCommand; 9 | 10 | /// 11 | /// A class with data reader factory methods. 12 | /// 13 | public static class ReaderFactory 14 | { 15 | /// 16 | /// A factory for creating TEntity objects from the current row in the . 17 | /// 18 | /// The type of the entity. 19 | /// The open to get the object from. 20 | /// A TEntity object having property names set that match the field names in the . 21 | public static TEntity EntityFactory(IDataReader reader) 22 | where TEntity : class, new() 23 | { 24 | var entityAccessor = TypeAccessor.GetAccessor(); 25 | var entity = new TEntity(); 26 | 27 | for (int i = 0; i < reader.FieldCount; i++) 28 | { 29 | if (reader.IsDBNull(i)) 30 | continue; 31 | 32 | var name = reader.GetName(i); 33 | 34 | var memberAccessor = entityAccessor.FindColumn(name); 35 | if (memberAccessor == null) 36 | continue; 37 | 38 | var value = reader.GetValue(i); 39 | var fieldType = reader.GetFieldType(i); 40 | 41 | var coerceValue = ReflectionHelper.CoerceValue(memberAccessor.MemberType, fieldType, value); 42 | memberAccessor.SetValue(entity, coerceValue); 43 | } 44 | 45 | return entity; 46 | } 47 | 48 | /// 49 | /// A factory for creating dynamic objects from the current row in the . 50 | /// 51 | /// The open to get the object from. 52 | /// A dynamic object having property names set that match the field names in the . 53 | public static dynamic DynamicFactory(IDataReader reader) 54 | { 55 | dynamic expando = new ExpandoObject(); 56 | var dictionary = expando as IDictionary; 57 | 58 | for (int i = 0; i < reader.FieldCount; i++) 59 | dictionary.Add(reader.GetName(i), reader[i]); 60 | 61 | return expando; 62 | } 63 | } -------------------------------------------------------------------------------- /src/FluentCommand.Generators/DiagnosticDescriptors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace FluentCommand.Generators; 4 | 5 | public static class DiagnosticDescriptors 6 | { 7 | public static DiagnosticDescriptor InvalidConstructor { get; } = new DiagnosticDescriptor( 8 | id: "FCG1001", 9 | title: "Invalid Constructor", 10 | messageFormat: "Count not find a constructor with {0} parameter(s) for type {1}. Classes initialized via constructor need to have the same number of parameters as public properties.", 11 | category: "FluentCommandGenerator", 12 | DiagnosticSeverity.Error, 13 | isEnabledByDefault: true); 14 | 15 | public static DiagnosticDescriptor InvalidConstructorParameter { get; } = new DiagnosticDescriptor( 16 | id: "FCG1002", 17 | title: "Invalid Constructor Parameter", 18 | messageFormat: "Count not find a constructor parameter {0} for type {1}. Classes initialized via constructor need to have parameters that match the properties in the class", 19 | category: "FluentCommandGenerator", 20 | DiagnosticSeverity.Warning, 21 | isEnabledByDefault: true); 22 | } 23 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/FluentCommand.Generators.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | true 7 | 8 | true 9 | true 10 | false 11 | true 12 | false 13 | 14 | cs 15 | 4.4 16 | 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/GenerateAttributeGenerator.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Generators.Models; 2 | 3 | using Microsoft.CodeAnalysis; 4 | 5 | namespace FluentCommand.Generators; 6 | 7 | [Generator(LanguageNames.CSharp)] 8 | public class GenerateAttributeGenerator : DataReaderFactoryGenerator, IIncrementalGenerator 9 | { 10 | public void Initialize(IncrementalGeneratorInitializationContext context) 11 | { 12 | var provider = context.SyntaxProvider.ForAttributeWithMetadataName( 13 | fullyQualifiedMetadataName: "FluentCommand.Attributes.GenerateReaderAttribute", 14 | predicate: SyntacticPredicate, 15 | transform: SemanticTransform 16 | ) 17 | .Where(static context => context is not null); 18 | 19 | // Emit the diagnostics, if needed 20 | var diagnostics = provider 21 | .Select(static (item, _) => item.Diagnostics) 22 | .Where(static item => item.Count > 0); 23 | 24 | context.RegisterSourceOutput(diagnostics, ReportDiagnostic); 25 | 26 | var entityClasses = provider 27 | .SelectMany(static (item, _) => item.EntityClasses) 28 | .Where(static item => item is not null); 29 | 30 | context.RegisterSourceOutput(entityClasses, WriteSource); 31 | } 32 | 33 | private static bool SyntacticPredicate(SyntaxNode syntaxNode, CancellationToken cancellationToken) 34 | { 35 | return true; 36 | } 37 | 38 | private static EntityContext SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) 39 | { 40 | if (context.Attributes.Length == 0) 41 | return null; 42 | 43 | var classes = new List(); 44 | var diagnostics = new List(); 45 | 46 | foreach (var attribute in context.Attributes) 47 | { 48 | if (attribute == null) 49 | return null; 50 | 51 | if (attribute.ConstructorArguments.Length != 1) 52 | return null; 53 | 54 | var comparerArgument = attribute.ConstructorArguments[0]; 55 | if (comparerArgument.Value is not INamedTypeSymbol targetSymbol) 56 | return null; 57 | 58 | var entityClass = CreateClass(context.TargetNode.GetLocation(), targetSymbol, diagnostics); 59 | if (entityClass != null) 60 | classes.Add(entityClass); 61 | } 62 | 63 | return new EntityContext(classes, diagnostics); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace System.Runtime.CompilerServices; 4 | 5 | [EditorBrowsable(EditorBrowsableState.Never)] 6 | internal static class IsExternalInit; 7 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/Models/EntityClass.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Generators.Models; 2 | 3 | public record EntityClass( 4 | InitializationMode InitializationMode, 5 | string FullyQualified, 6 | string EntityNamespace, 7 | string EntityName, 8 | EquatableArray Properties 9 | ); 10 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/Models/EntityContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace FluentCommand.Generators.Models; 4 | 5 | public record EntityContext( 6 | EquatableArray EntityClasses, 7 | EquatableArray Diagnostics 8 | ); 9 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/Models/EntityProperty.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Generators.Models; 2 | 3 | public record EntityProperty( 4 | string PropertyName, 5 | string ColumnName, 6 | string PropertyType, 7 | string ParameterName = null, 8 | string ConverterName = null 9 | ); 10 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/Models/EquatableArray.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | #nullable enable 5 | 6 | namespace FluentCommand.Generators.Models; 7 | 8 | [ExcludeFromCodeCoverage] 9 | public readonly struct EquatableArray : IEquatable>, IEnumerable 10 | where T : IEquatable 11 | { 12 | public static readonly EquatableArray Empty = new(); 13 | 14 | 15 | public EquatableArray() : this([]) { } 16 | 17 | public EquatableArray(T[] array) => Array = array ?? []; 18 | 19 | public EquatableArray(IEnumerable items) => Array = items.ToArray() ?? []; 20 | 21 | 22 | public T[] Array { get; } 23 | 24 | public int Count => Array.Length; 25 | 26 | 27 | public ReadOnlySpan AsSpan() => Array.AsSpan(); 28 | 29 | public T[] AsArray() => Array; 30 | 31 | 32 | public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right); 33 | 34 | public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right); 35 | 36 | public bool Equals(EquatableArray array) => Array.AsSpan().SequenceEqual(array.AsSpan()); 37 | 38 | public override bool Equals(object? obj) => obj is EquatableArray array && Equals(this, array); 39 | 40 | public override int GetHashCode() 41 | { 42 | if (Array is not T[] array) 43 | return 0; 44 | 45 | var hashCode = 16777619; 46 | 47 | for (int i = 0; i < array.Length; i++) 48 | hashCode = unchecked((hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(array[i])); 49 | 50 | return hashCode; 51 | } 52 | 53 | 54 | IEnumerator IEnumerable.GetEnumerator() => (Array as IEnumerable).GetEnumerator(); 55 | 56 | IEnumerator IEnumerable.GetEnumerator() => Array.GetEnumerator(); 57 | 58 | 59 | public static implicit operator EquatableArray(T[] array) => new(array); 60 | 61 | public static implicit operator EquatableArray(List items) => new(items); 62 | } 63 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/Models/InitializationMode.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Generators.Models; 2 | 3 | public enum InitializationMode 4 | { 5 | ObjectInitializer, 6 | Constructor 7 | } 8 | -------------------------------------------------------------------------------- /src/FluentCommand.Generators/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "FluentCommand.Tests": { 4 | "commandName": "DebugRoslynComponent", 5 | "targetProject": "..\\..\\test\\FluentCommand.Tests\\FluentCommand.Tests.csproj" 6 | }, 7 | "FluentCommand.Entities": { 8 | "commandName": "DebugRoslynComponent", 9 | "targetProject": "..\\..\\test\\FluentCommand.Entities\\FluentCommand.Entities.csproj" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/FluentCommand.Json/ConcurrencyTokenJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace FluentCommand; 5 | 6 | /// 7 | /// Json Converter for 8 | /// 9 | public class ConcurrencyTokenJsonConverter : JsonConverter 10 | { 11 | /// 12 | /// Read and convert the JSON to T. 13 | /// 14 | /// The to read from. 15 | /// The being converted. 16 | /// The being used. 17 | /// 18 | /// The value that was converted. 19 | /// 20 | /// 21 | /// A converter may throw any Exception, but should throw JsonException when the JSON is invalid. 22 | /// 23 | public override ConcurrencyToken Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 24 | => new(reader.GetString()); 25 | 26 | /// 27 | /// Write the value as JSON. 28 | /// 29 | /// The to write to. 30 | /// The value to convert. Note that the value of determines if the converter handles values. 31 | /// The being used. 32 | /// 33 | /// A converter may throw any Exception, but should throw JsonException when the JSON 34 | /// cannot be created. 35 | /// 36 | public override void Write(Utf8JsonWriter writer, ConcurrencyToken value, JsonSerializerOptions options) 37 | => writer.WriteStringValue(value.ToString()); 38 | } 39 | -------------------------------------------------------------------------------- /src/FluentCommand.Json/FluentCommand.Json.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net8.0;net9.0 4 | FluentCommand 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Bulk/DataBulkCopyExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | 3 | namespace FluentCommand.Bulk; 4 | 5 | /// 6 | /// Bulk Copy extension methods 7 | /// 8 | public static class DataBulkCopyExtensions 9 | { 10 | /// 11 | /// Starts a data bulk-copy with the specified destination table name. 12 | /// 13 | /// The session to use for the bulk-copy. 14 | /// Name of the destination table on the server. 15 | /// 16 | /// A fluent to a operation. 17 | /// 18 | public static IDataBulkCopy BulkCopy(this IDataSession session, string destinationTable) 19 | { 20 | var bulkCopy = new DataBulkCopy(session, destinationTable); 21 | return bulkCopy; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/DataConfigurationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | 3 | namespace FluentCommand; 4 | 5 | public static class DataConfigurationBuilderExtensions 6 | { 7 | public static DataConfigurationBuilder UseSqlServer(this DataConfigurationBuilder builder) 8 | { 9 | builder 10 | .AddProviderFactory(SqlClientFactory.Instance) 11 | .AddSqlServerGenerator(); 12 | 13 | return builder; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/FluentCommand.SqlServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net462;net8.0;net9.0 4 | FluentCommand 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/FieldDefault.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// Default field value 5 | /// 6 | public enum FieldDefault 7 | { 8 | /// 9 | /// Use the current username as the default value. 10 | /// 11 | UserName, 12 | /// 13 | /// Use the current UTC date time as the default value. 14 | /// 15 | CurrentDate, 16 | /// 17 | /// Use a static default value. 18 | /// 19 | Static 20 | } -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/FieldMap.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// Import field mapping 5 | /// 6 | public class FieldMap 7 | { 8 | /// 9 | /// Gets or sets the field index. 10 | /// 11 | /// 12 | /// The field index. 13 | /// 14 | public int? Index { get; set; } 15 | 16 | /// 17 | /// Gets or sets the name of the field. 18 | /// 19 | /// 20 | /// The name of the field. 21 | /// 22 | public string Name { get; set; } 23 | 24 | /// 25 | /// Returns a that represents this instance. 26 | /// 27 | /// 28 | /// A that represents this instance. 29 | /// 30 | public override string ToString() 31 | { 32 | return $"Name: {Name}, Index: {Index}"; 33 | } 34 | } -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/IFieldTranslator.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// An interface for translating a field value 5 | /// 6 | public interface IFieldTranslator 7 | { 8 | /// 9 | /// Translates the specified original value. 10 | /// 11 | /// The original value. 12 | /// The translated value 13 | Task Translate(string original); 14 | } 15 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/IImportProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// A data import processor 5 | /// 6 | public interface IImportProcessor 7 | { 8 | /// 9 | /// Import data using the specified and . 10 | /// 11 | /// The import definition. 12 | /// The import data. 13 | /// The name of the user importing the data. 14 | /// The cancellation token. 15 | /// The results of the import 16 | /// or is null 17 | Task ImportAsync(ImportDefinition importDefinition, ImportData importData, string username, CancellationToken cancellationToken = default); 18 | } 19 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/IImportValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand.Import; 4 | 5 | /// 6 | /// An import data validator 7 | /// 8 | public interface IImportValidator 9 | { 10 | /// 11 | /// Validates the specified . 12 | /// 13 | /// The import definition. 14 | /// The target row. 15 | /// 16 | Task ValidateRow(ImportDefinition importDefinition, DataRow targetRow); 17 | } 18 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/ImportData.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// The data and field mapping of an import 5 | /// 6 | public class ImportData 7 | { 8 | /// 9 | /// Gets or sets the name of the import file. 10 | /// 11 | /// 12 | /// The name of the file. 13 | /// 14 | public string FileName { get; set; } 15 | 16 | /// 17 | /// Gets or sets the data to be imported. 18 | /// 19 | /// 20 | /// The data to be imported. 21 | /// 22 | public string[][] Data { get; set; } 23 | 24 | /// 25 | /// Gets or sets the list of field mappings. 26 | /// 27 | /// 28 | /// The list of field mappings. 29 | /// 30 | public List Mappings { get; set; } 31 | 32 | /// 33 | /// Gets or sets a value indicating whether data has a header row. 34 | /// 35 | /// 36 | /// true if data has a header row; otherwise, false. 37 | /// 38 | public bool HasHeader { get; set; } = true; 39 | } 40 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/ImportFactory.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// Factory method used to resolve all services. For multiple instances, it will resolve against 5 | /// 6 | /// Type of service to resolve 7 | /// An instance of type 8 | public delegate object ImportFactory(Type serviceType); 9 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/ImportFieldMapping.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// Import field mapping 5 | /// 6 | public class ImportFieldMapping 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | /// The definition. 12 | public ImportFieldMapping(FieldDefinition fieldDefinition) : this(fieldDefinition, null) 13 | { 14 | } 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The field definition. 20 | /// The field field map. 21 | /// definition is null 22 | public ImportFieldMapping(FieldDefinition fieldDefinition, FieldMap fieldMap) 23 | { 24 | Definition = fieldDefinition ?? throw new ArgumentNullException(nameof(fieldDefinition)); 25 | FieldMap = fieldMap; 26 | } 27 | 28 | /// 29 | /// Gets the field definition. 30 | /// 31 | /// 32 | /// The field definition. 33 | /// 34 | public FieldDefinition Definition { get; } 35 | 36 | /// 37 | /// Gets the field mapping. 38 | /// 39 | /// 40 | /// The field mapping. 41 | /// 42 | public FieldMap FieldMap { get; } 43 | } 44 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Import/ImportResult.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Import; 2 | 3 | /// 4 | /// The result of the import 5 | /// 6 | public class ImportResult 7 | { 8 | /// 9 | /// Gets or sets the message. 10 | /// 11 | /// 12 | /// The message. 13 | /// 14 | public string Message { get; set; } 15 | 16 | /// 17 | /// Gets or sets the number of rows processed. 18 | /// 19 | /// 20 | /// The number of rows processed. 21 | /// 22 | public int Processed { get; set; } 23 | 24 | /// 25 | /// Gets or sets the list of errors. 26 | /// 27 | /// 28 | /// The list of errors. 29 | /// 30 | public IReadOnlyCollection Errors { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Merge/DataMergeMode.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Merge; 2 | 3 | /// 4 | /// How the data should be merged into the table 5 | /// 6 | public enum DataMergeMode 7 | { 8 | /// 9 | /// Automatic determine the best mode. If source over 1000 row, will be used. 10 | /// 11 | Auto, 12 | /// 13 | /// Use SqlServer bulk copy to insert rows into a temporary table, then merge to target table 14 | /// 15 | BulkCopy, 16 | /// 17 | /// Generate a SqlServer merge statement to execute. 18 | /// 19 | SqlStatement 20 | } 21 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Merge/DataMergeOutputColumn.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Merge; 2 | 3 | /// 4 | /// A data merge output column. 5 | /// 6 | public class DataMergeOutputColumn 7 | { 8 | /// 9 | /// Gets or sets the name of the column. 10 | /// 11 | /// 12 | /// The name of the column. 13 | /// 14 | public string Name { get; set; } 15 | 16 | /// 17 | /// Gets or sets the current value of the column. 18 | /// 19 | /// 20 | /// The current value of the column. 21 | /// 22 | public object Current { get; set; } 23 | 24 | /// 25 | /// Gets or sets the original value of the column. 26 | /// 27 | /// 28 | /// The original value of the column. 29 | /// 30 | public object Original { get; set; } 31 | 32 | /// 33 | /// Gets or sets the . 34 | /// 35 | /// 36 | /// The column value type. 37 | /// 38 | public Type Type { get; set; } 39 | } 40 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Merge/DataMergeOutputRow.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Merge; 2 | 3 | /// 4 | /// A data merge output row. 5 | /// 6 | public class DataMergeOutputRow 7 | { 8 | /// 9 | /// Initializes a new instance of the class. 10 | /// 11 | public DataMergeOutputRow() 12 | { 13 | Columns = new List(); 14 | } 15 | 16 | /// 17 | /// Gets or sets the merge action. 18 | /// 19 | /// 20 | /// The merge action for this row. 21 | /// 22 | public string Action { get; set; } 23 | 24 | /// 25 | /// Gets or sets the columns that changed. 26 | /// 27 | /// 28 | /// The list of columns. 29 | /// 30 | public List Columns { get; set; } 31 | 32 | /// 33 | /// Gets the with the specified column name. 34 | /// 35 | /// 36 | /// The . 37 | /// 38 | /// Name of the column. 39 | /// 40 | public DataMergeOutputColumn this[string columnName] 41 | { 42 | get 43 | { 44 | return Columns.FirstOrDefault(c => c.Name == columnName); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Merge/IDataMergeMapping.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace FluentCommand.Merge; 4 | 5 | /// 6 | /// A fluent for a data merge mapping. 7 | /// 8 | public interface IDataMergeMapping 9 | { 10 | /// 11 | /// Start column mapping for the specified source column name. 12 | /// 13 | /// The source column name. 14 | /// 15 | /// A fluent for a data merge mapping. 16 | /// 17 | IDataColumnMapping Column(string sourceColumn); 18 | } 19 | 20 | /// 21 | /// A fluent for a strongly typed data merge mapping. 22 | /// 23 | /// The type of entity being mapped 24 | public interface IDataMergeMapping : IDataMergeMapping 25 | { 26 | /// 27 | /// Automatically maps all properties in as columns. 28 | /// 29 | /// 30 | /// 31 | IDataMergeMapping AutoMap(); 32 | 33 | /// 34 | /// Start column mapping for the specified source column name. 35 | /// 36 | /// The property value type. 37 | /// The source property. 38 | /// 39 | /// A fluent for a data merge mapping. 40 | /// 41 | IDataColumnMapping Column(Expression> sourceProperty); 42 | } 43 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Query/ChangeTableBuilder.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Extensions; 2 | using FluentCommand.Query.Generators; 3 | using FluentCommand.Reflection; 4 | 5 | namespace FluentCommand.Query; 6 | 7 | public class ChangeTableBuilder : StatementBuilder 8 | { 9 | private TableExpression _fromTable; 10 | private QueryParameter _parameter; 11 | 12 | public ChangeTableBuilder(IQueryGenerator queryGenerator, List parameters) : base(queryGenerator, parameters) 13 | { 14 | } 15 | 16 | public ChangeTableBuilder From( 17 | string tableName, 18 | string tableSchema = null, 19 | string tableAlias = null) 20 | { 21 | _fromTable = new TableExpression(tableName, tableSchema, tableAlias); 22 | 23 | return this; 24 | } 25 | 26 | public ChangeTableBuilder From( 27 | string tableAlias = null) 28 | { 29 | var typeAccessor = TypeAccessor.GetAccessor(); 30 | 31 | _fromTable = new TableExpression(typeAccessor.TableName, typeAccessor.TableSchema, tableAlias); 32 | 33 | return this; 34 | } 35 | 36 | public ChangeTableBuilder LastVersion(long lastVersion) 37 | { 38 | var name = NextParameter(); 39 | _parameter = new QueryParameter(name, lastVersion, typeof(long)); 40 | 41 | Parameters.Add(_parameter); 42 | 43 | return this; 44 | } 45 | 46 | public override QueryStatement BuildStatement() 47 | { 48 | if (_parameter == null) 49 | return new QueryStatement(QueryGenerator.TableExpression(_fromTable), Parameters); 50 | 51 | var table = QueryGenerator.TableExpression(new TableExpression(_fromTable.TableName, _fromTable.TableSchema)); 52 | 53 | var statement = $"CHANGETABLE (CHANGES {table}, {_parameter.Name})"; 54 | 55 | if (_fromTable.TableAlias.HasValue()) 56 | statement += $" AS [{_fromTable.TableAlias}]"; 57 | 58 | return new QueryStatement(statement, Parameters); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Query/ChangeTableBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | public static class ChangeTableBuilderExtensions 4 | { 5 | public static SelectBuilder ChangeTable(this SelectBuilder selectBuilder, Action builder) 6 | { 7 | if (selectBuilder is null) 8 | throw new ArgumentNullException(nameof(selectBuilder)); 9 | if (builder is null) 10 | throw new ArgumentNullException(nameof(builder)); 11 | 12 | var queryBuilder = selectBuilder as IQueryBuilder; 13 | 14 | var innerBuilder = new ChangeTableBuilder(queryBuilder.QueryGenerator, queryBuilder.Parameters); 15 | builder(innerBuilder); 16 | 17 | var statement = innerBuilder.BuildStatement(); 18 | selectBuilder.FromRaw(statement.Statement); 19 | 20 | return selectBuilder; 21 | 22 | } 23 | 24 | public static SelectEntityBuilder ChangeTable(this SelectEntityBuilder selectBuilder, Action builder) 25 | where TEntity : class 26 | { 27 | if (selectBuilder is null) 28 | throw new ArgumentNullException(nameof(selectBuilder)); 29 | if (builder is null) 30 | throw new ArgumentNullException(nameof(builder)); 31 | 32 | var queryBuilder = selectBuilder as IQueryBuilder; 33 | 34 | var innerBuilder = new ChangeTableBuilder(queryBuilder.QueryGenerator, queryBuilder.Parameters); 35 | 36 | // preset table and schema 37 | innerBuilder.From(); 38 | 39 | builder(innerBuilder); 40 | 41 | var statement = innerBuilder.BuildStatement(); 42 | selectBuilder.FromRaw(statement.Statement); 43 | 44 | return selectBuilder; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/Query/TemporalBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | public static class TemporalBuilderExtensions 4 | { 5 | public static SelectBuilder Temporal(this SelectBuilder selectBuilder, Action builder) 6 | { 7 | if (selectBuilder is null) 8 | throw new ArgumentNullException(nameof(selectBuilder)); 9 | if (builder is null) 10 | throw new ArgumentNullException(nameof(builder)); 11 | 12 | var queryBuilder = selectBuilder as IQueryBuilder; 13 | 14 | var innerBuilder = new TemporalBuilder(queryBuilder.QueryGenerator, queryBuilder.Parameters); 15 | builder(innerBuilder); 16 | 17 | var statement = innerBuilder.BuildStatement(); 18 | selectBuilder.FromRaw(statement.Statement); 19 | 20 | return selectBuilder; 21 | 22 | } 23 | 24 | public static SelectEntityBuilder Temporal(this SelectEntityBuilder selectBuilder, Action builder) 25 | where TEntity : class 26 | { 27 | if (selectBuilder is null) 28 | throw new ArgumentNullException(nameof(selectBuilder)); 29 | if (builder is null) 30 | throw new ArgumentNullException(nameof(builder)); 31 | 32 | var queryBuilder = selectBuilder as IQueryBuilder; 33 | 34 | var innerBuilder = new TemporalBuilder(queryBuilder.QueryGenerator, queryBuilder.Parameters); 35 | 36 | // preset table and schema 37 | innerBuilder.From(); 38 | 39 | builder(innerBuilder); 40 | 41 | var statement = innerBuilder.BuildStatement(); 42 | selectBuilder.FromRaw(statement.Statement); 43 | 44 | return selectBuilder; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/SqlCommandExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using FluentCommand.Extensions; 4 | 5 | using Microsoft.Data.SqlClient; 6 | 7 | namespace FluentCommand; 8 | 9 | /// 10 | /// Extension methods for specific to Sql Server. 11 | /// 12 | public static class SqlCommandExtensions 13 | { 14 | /// 15 | /// Adds a new Sql Server structured table-valued parameter with the specified and . 16 | /// 17 | /// The for this extension method. 18 | /// The name of the parameter. 19 | /// The data to be added. 20 | /// 21 | /// A fluent to the data command. 22 | /// 23 | public static IDataCommand SqlParameter(this IDataCommand dataCommand, string name, IEnumerable data) 24 | where TEntity : class 25 | { 26 | var dataTable = data.ToDataTable(); 27 | return SqlParameter(dataCommand, name, dataTable); 28 | } 29 | 30 | /// 31 | /// Adds a new Sql Server structured table-valued parameter with the specified and . 32 | /// 33 | /// The for this extension method. 34 | /// The name of the parameter. 35 | /// The to be added. 36 | /// 37 | /// A fluent to the data command. 38 | /// 39 | public static IDataCommand SqlParameter(this IDataCommand dataCommand, string name, DataTable dataTable) 40 | { 41 | var parameter = dataCommand.Command.CreateParameter(); 42 | var sqlParameter = parameter as SqlParameter; 43 | if (sqlParameter == null) 44 | throw new InvalidOperationException( 45 | "SqlParameter only supported by SQL Server. Make sure DataSession was created with a valid SqlConnection."); 46 | 47 | sqlParameter.ParameterName = name; 48 | sqlParameter.Value = dataTable; 49 | sqlParameter.Direction = ParameterDirection.Input; 50 | sqlParameter.SqlDbType = SqlDbType.Structured; 51 | 52 | return dataCommand.Parameter(sqlParameter); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/FluentCommand.SqlServer/SqlTypeMapping.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand; 2 | 3 | /// 4 | /// Sql Server type mapping 5 | /// 6 | public static class SqlTypeMapping 7 | { 8 | private static readonly Dictionary _nativeType = new Dictionary 9 | { 10 | {typeof(bool), "bit"}, 11 | {typeof(byte), "tinyint"}, 12 | {typeof(short), "smallint"}, 13 | {typeof(int), "int"}, 14 | {typeof(long), "bigint"}, 15 | {typeof(float), "real"}, 16 | {typeof(double), "float"}, 17 | {typeof(decimal), "decimal"}, 18 | {typeof(byte[]), "varbinary(MAX)"}, 19 | {typeof(string), "nvarchar(MAX)"}, 20 | {typeof(TimeSpan), "time"}, 21 | {typeof(DateTime), "datetime2"}, 22 | {typeof(DateTimeOffset), "datetimeoffset"}, 23 | {typeof(Guid), "uniqueidentifier"}, 24 | #if NET6_0_OR_GREATER 25 | {typeof(DateOnly), "date"}, 26 | {typeof(TimeOnly), "time"}, 27 | #endif 28 | }; 29 | 30 | /// 31 | /// Converts the .NET Type into sql server native type 32 | /// 33 | /// The type to convert 34 | /// The SQL Server native type 35 | public static string NativeType() 36 | { 37 | return NativeType(typeof(T)); 38 | } 39 | 40 | /// 41 | /// Converts the .NET Type into sql server native type 42 | /// 43 | /// The type to convert 44 | /// The SQL Server native type 45 | public static string NativeType(Type type) 46 | { 47 | var dataType = Nullable.GetUnderlyingType(type) ?? type; 48 | 49 | _nativeType.TryGetValue(dataType, out var value); 50 | 51 | return value ?? "sql_variant"; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/FluentCommand/Attributes/GenerateReaderAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace FluentCommand.Attributes; 4 | 5 | /// 6 | /// Source generate a data reader for the specified type 7 | /// 8 | /// The type to generate a data reader for 9 | [Conditional("FLUENTCOMMAND_GENERATOR")] 10 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Module, AllowMultiple = true)] 11 | public class GenerateReaderAttribute(Type type) : Attribute 12 | { 13 | public Type Type { get; } = type ?? throw new ArgumentNullException(nameof(type)); 14 | } 15 | -------------------------------------------------------------------------------- /src/FluentCommand/DataCallback.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | using FluentCommand.Extensions; 4 | 5 | namespace FluentCommand; 6 | 7 | internal class DataCallback 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The type. 13 | /// The parameter. 14 | /// The callback. 15 | public DataCallback(Type type, DbParameter parameter, Delegate callback) 16 | { 17 | Type = type ?? throw new ArgumentNullException(nameof(type)); 18 | Parameter = parameter ?? throw new ArgumentNullException(nameof(parameter)); 19 | Callback = callback ?? throw new ArgumentNullException(nameof(callback)); 20 | } 21 | 22 | /// 23 | /// Gets or sets the type of the call back value. 24 | /// 25 | public Type Type { get; } 26 | /// 27 | /// Gets or sets the callback . 28 | /// 29 | public Delegate Callback { get; } 30 | /// 31 | /// Gets or sets the parameter associated with the callback. 32 | /// 33 | public DbParameter Parameter { get; } 34 | 35 | /// 36 | /// Invokes the with the value. 37 | /// 38 | public void Invoke() 39 | { 40 | var handler = DataParameterHandlers.GetTypeHandler(Type); 41 | 42 | var value = handler?.ReadValue(Parameter) ?? Parameter.Value; 43 | if (value == DBNull.Value) 44 | value = Type.Default(); 45 | 46 | Callback.DynamicInvoke(value); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/FluentCommand/DataFieldConverterAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand; 2 | 3 | /// 4 | /// Attribute to enable source generation of data reader factory 5 | /// 6 | /// 7 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] 8 | public class DataFieldConverterAttribute : Attribute 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Type of the converter. 14 | public DataFieldConverterAttribute(Type converterType) 15 | { 16 | ConverterType = converterType; 17 | } 18 | 19 | /// 20 | /// Gets the type of the converter. 21 | /// 22 | /// 23 | /// The type of the converter. 24 | /// 25 | public Type ConverterType { get; } 26 | } 27 | 28 | #if NET7_0_OR_GREATER 29 | /// 30 | /// Attribute to enable source generation of data reader factory 31 | /// 32 | /// 33 | /// The type of the converter 34 | /// 35 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] 36 | public class DataFieldConverterAttribute : DataFieldConverterAttribute 37 | { 38 | /// 39 | /// Initializes a new instance of the class. 40 | /// 41 | public DataFieldConverterAttribute() : base(typeof(TConverter)) 42 | { 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /src/FluentCommand/DataReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Dynamic; 3 | 4 | using FluentCommand.Reflection; 5 | 6 | namespace FluentCommand; 7 | 8 | /// 9 | /// Extension methods for 10 | /// 11 | public static class DataReaderExtensions 12 | { 13 | /// 14 | /// A factory for creating TEntity objects from the current row in the . 15 | /// 16 | /// The type of the entity. 17 | /// The open to get the object from. 18 | /// A TEntity object having property names set that match the field names in the . 19 | [Obsolete("Use generated data reader factory")] 20 | public static TEntity EntityFactory(this IDataReader reader) 21 | where TEntity : class, new() 22 | { 23 | var entityAccessor = TypeAccessor.GetAccessor(); 24 | var entity = new TEntity(); 25 | 26 | for (int i = 0; i < reader.FieldCount; i++) 27 | { 28 | if (reader.IsDBNull(i)) 29 | continue; 30 | 31 | var name = reader.GetName(i); 32 | 33 | var memberAccessor = entityAccessor.FindColumn(name); 34 | if (memberAccessor == null) 35 | continue; 36 | 37 | var value = reader.GetValue(i); 38 | 39 | memberAccessor.SetValue(entity, value); 40 | } 41 | 42 | return entity; 43 | } 44 | 45 | /// 46 | /// A factory for creating dynamic objects from the current row in the . 47 | /// 48 | /// The open to get the object from. 49 | /// A dynamic object having property names set that match the field names in the . 50 | public static dynamic DynamicFactory(IDataReader reader) 51 | { 52 | dynamic expando = new ExpandoObject(); 53 | var dictionary = expando as IDictionary; 54 | 55 | for (int i = 0; i < reader.FieldCount; i++) 56 | dictionary.Add(reader.GetName(i), reader[i]); 57 | 58 | return expando; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/FluentCommand/Extensions/DataTableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data; 3 | 4 | namespace FluentCommand.Extensions; 5 | 6 | /// 7 | /// Extension method for 8 | /// 9 | public static class DataTableExtensions 10 | { 11 | /// 12 | /// Converts the IEnumerable to a . 13 | /// 14 | /// The type of the source data 15 | /// The source to convert. 16 | /// The ignored property names. 17 | /// A from the specified source. 18 | public static DataTable ToDataTable(this IEnumerable source, IEnumerable ignoreNames = null) 19 | where T : class 20 | { 21 | if (source == null) 22 | return null; 23 | 24 | using var dataReader = new ListDataReader(source, ignoreNames); 25 | 26 | var dataTable = new DataTable(); 27 | dataTable.Load(dataReader); 28 | 29 | return dataTable; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/FluentCommand/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | #nullable enable 4 | 5 | namespace FluentCommand.Extensions; 6 | 7 | /// 8 | /// extension methods 9 | /// 10 | public static class StringExtensions 11 | { 12 | /// 13 | /// Indicates whether the specified String object is null or an empty string 14 | /// 15 | /// A String reference 16 | /// 17 | /// true if is null or empty; otherwise, false. 18 | /// 19 | public static bool IsNullOrEmpty([NotNullWhen(false)] this string? item) 20 | { 21 | return string.IsNullOrEmpty(item); 22 | } 23 | 24 | /// 25 | /// Indicates whether a specified string is null, empty, or consists only of white-space characters 26 | /// 27 | /// A String reference 28 | /// 29 | /// true if is null or empty; otherwise, false. 30 | /// 31 | public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? item) 32 | { 33 | if (item == null) 34 | return true; 35 | 36 | for (int i = 0; i < item.Length; i++) 37 | if (!char.IsWhiteSpace(item[i])) 38 | return false; 39 | 40 | return true; 41 | } 42 | 43 | /// 44 | /// Determines whether the specified string is not . 45 | /// 46 | /// The value to check. 47 | /// 48 | /// true if the specified is not ; otherwise, false. 49 | /// 50 | public static bool HasValue([NotNullWhen(true)] this string? value) 51 | { 52 | return !string.IsNullOrEmpty(value); 53 | } 54 | 55 | /// 56 | /// Replaces the format item in a specified string with the string representation of a corresponding object in a specified array. 57 | /// 58 | /// A composite format string 59 | /// An object array that contains zero or more objects to format 60 | /// A copy of format in which the format items have been replaced by the string representation of the corresponding objects in args 61 | public static string FormatWith(this string format, params object?[] args) 62 | { 63 | return string.Format(format, args); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/FluentCommand/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Extensions; 2 | 3 | /// 4 | /// extension methods. 5 | /// 6 | public static class TypeExtensions 7 | { 8 | /// 9 | /// Gets the underlying type dealing with . 10 | /// 11 | /// The type. 12 | /// Returns a type dealing with . 13 | public static Type GetUnderlyingType(this Type type) 14 | { 15 | if (type == null) 16 | throw new ArgumentNullException(nameof(type)); 17 | 18 | var isNullable = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); 19 | return isNullable ? Nullable.GetUnderlyingType(type) : type; 20 | } 21 | 22 | /// 23 | /// Determines whether the specified can be null. 24 | /// 25 | /// The type to check. 26 | /// 27 | /// true if the specified can be null; otherwise, false. 28 | /// 29 | public static bool IsNullable(this Type type) 30 | { 31 | if (type == null) 32 | throw new ArgumentNullException(nameof(type)); 33 | 34 | if (!type.IsValueType) 35 | return true; 36 | 37 | return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); 38 | } 39 | 40 | /// 41 | /// Gets the default value the specified . 42 | /// 43 | /// The type to get a default value for. 44 | /// A default value the specified . 45 | public static object Default(this Type type) 46 | { 47 | if (type == null) 48 | throw new ArgumentNullException(nameof(type)); 49 | 50 | return type.IsValueType 51 | ? Activator.CreateInstance(type) 52 | : null; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/FluentCommand/FluentCommand.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;net8.0;net9.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/FluentCommand/Handlers/ConcurrencyTokenHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using FluentCommand.Extensions; 4 | 5 | namespace FluentCommand.Handlers; 6 | 7 | /// 8 | /// A data type handler for 9 | /// 10 | /// 11 | public class ConcurrencyTokenHandler : IDataParameterHandler, IDataFieldConverter 12 | { 13 | /// 14 | public Type ValueType { get; } = typeof(ConcurrencyToken); 15 | 16 | /// 17 | public ConcurrencyToken ReadValue(IDataRecord dataRecord, int fieldIndex) 18 | { 19 | if (dataRecord.IsDBNull(fieldIndex)) 20 | return ConcurrencyToken.None; 21 | 22 | var bytes = dataRecord.GetBytes(fieldIndex); 23 | 24 | return new ConcurrencyToken(bytes); 25 | } 26 | 27 | /// 28 | public object ReadValue(IDbDataParameter parameter) 29 | { 30 | return parameter.Value switch 31 | { 32 | string textToken => new ConcurrencyToken(textToken), 33 | byte[] byteToken => new ConcurrencyToken(byteToken), 34 | _ => ConcurrencyToken.None 35 | }; 36 | } 37 | 38 | /// 39 | public void SetValue(IDbDataParameter parameter, object value) 40 | { 41 | parameter.DbType = DbType.Binary; 42 | parameter.Value = value switch 43 | { 44 | ConcurrencyToken concurrencyToken => concurrencyToken.Value, 45 | null => DBNull.Value, 46 | _ => value 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/FluentCommand/Handlers/DateOnlyHandler.cs: -------------------------------------------------------------------------------- 1 | #if NET6_0_OR_GREATER 2 | using System.Data; 3 | 4 | using FluentCommand; 5 | 6 | namespace FluentCommand.Handlers; 7 | 8 | /// 9 | /// A data type handler for 10 | /// 11 | /// 12 | public class DateOnlyHandler : IDataParameterHandler, IDataFieldConverter 13 | { 14 | /// 15 | public Type ValueType { get; } = typeof(DateOnly); 16 | 17 | /// 18 | public DateOnly ReadValue(IDataRecord dataRecord, int fieldIndex) 19 | { 20 | var value = dataRecord.GetValue(fieldIndex); 21 | 22 | return value switch 23 | { 24 | DateOnly dateOnly => dateOnly, 25 | DateTime dateTime => DateOnly.FromDateTime(dateTime), 26 | _ => default 27 | }; 28 | } 29 | 30 | /// 31 | public object ReadValue(IDbDataParameter parameter) 32 | { 33 | return parameter.Value switch 34 | { 35 | DateOnly dateOnly => dateOnly, 36 | DateTime dateTime => DateOnly.FromDateTime(dateTime), 37 | _ => default 38 | }; 39 | } 40 | 41 | /// 42 | public void SetValue(IDbDataParameter parameter, object value) 43 | { 44 | parameter.DbType = DbType.Date; 45 | parameter.Value = value switch 46 | { 47 | DateOnly dateOnly => dateOnly.ToDateTime(new TimeOnly(0, 0)), 48 | null => DBNull.Value, 49 | _ => value 50 | }; 51 | } 52 | } 53 | #endif 54 | -------------------------------------------------------------------------------- /src/FluentCommand/Handlers/TimeOnlyHandler.cs: -------------------------------------------------------------------------------- 1 | #if NET6_0_OR_GREATER 2 | using System.Data; 3 | 4 | using FluentCommand; 5 | 6 | namespace FluentCommand.Handlers; 7 | 8 | /// 9 | /// A data type handler for 10 | /// 11 | /// 12 | public class TimeOnlyHandler : IDataParameterHandler, IDataFieldConverter 13 | { 14 | /// 15 | public Type ValueType { get; } = typeof(TimeOnly); 16 | 17 | /// 18 | public TimeOnly ReadValue(IDataRecord dataRecord, int fieldIndex) 19 | { 20 | var value = dataRecord.GetValue(fieldIndex); 21 | 22 | return value switch 23 | { 24 | TimeOnly timeOnly => timeOnly, 25 | TimeSpan timeSpan => TimeOnly.FromTimeSpan(timeSpan), 26 | DateTime dateTime => TimeOnly.FromDateTime(dateTime), 27 | _ => default 28 | }; 29 | } 30 | 31 | /// 32 | public object ReadValue(IDbDataParameter parameter) 33 | { 34 | return parameter.Value switch 35 | { 36 | TimeOnly timeOnly => timeOnly, 37 | TimeSpan timeSpan => TimeOnly.FromTimeSpan(timeSpan), 38 | DateTime dateTime => TimeOnly.FromDateTime(dateTime), 39 | _ => default 40 | }; 41 | } 42 | 43 | /// 44 | public void SetValue(IDbDataParameter parameter, object value) 45 | { 46 | parameter.DbType = DbType.Time; 47 | parameter.Value = value switch 48 | { 49 | TimeOnly timeOnly => timeOnly.ToTimeSpan(), 50 | null => DBNull.Value, 51 | _ => value 52 | }; 53 | } 54 | } 55 | #endif 56 | -------------------------------------------------------------------------------- /src/FluentCommand/IDataConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | using FluentCommand.Query.Generators; 4 | 5 | namespace FluentCommand; 6 | 7 | /// 8 | /// An interface for database configuration 9 | /// 10 | public interface IDataConfiguration : IDataSessionFactory 11 | { 12 | /// 13 | /// Gets the database provider factory. 14 | /// 15 | /// 16 | /// The provider factory. 17 | /// 18 | DbProviderFactory ProviderFactory { get; } 19 | 20 | /// 21 | /// Gets the database connection string. 22 | /// 23 | /// 24 | /// The connection string. 25 | /// 26 | string ConnectionString { get; } 27 | 28 | /// 29 | /// Gets the data command query logger. 30 | /// 31 | /// 32 | /// The data command query logger. 33 | /// 34 | IDataQueryLogger QueryLogger { get; } 35 | 36 | /// 37 | /// Gets the data cache manager. 38 | /// 39 | /// 40 | /// The data cache manager. 41 | /// 42 | IDataCache DataCache { get; } 43 | 44 | /// 45 | /// Gets the query generator provider. 46 | /// 47 | /// 48 | /// The query generator provider. 49 | /// 50 | IQueryGenerator QueryGenerator { get; } 51 | 52 | /// 53 | /// Creates a new instance from this database configuration. 54 | /// 55 | /// 56 | /// The connection string to use for the session. If is null, will be used. 57 | /// A new instance. 58 | /// 59 | DbConnection CreateConnection(string connectionString = null); 60 | } 61 | 62 | /// 63 | /// The database configuration by discriminator. Used to register multiple instances of IDataConfiguration. 64 | /// 65 | /// The type of the discriminator. 66 | public interface IDataConfiguration : IDataConfiguration, IDataSessionFactory 67 | { 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/FluentCommand/IDataFieldConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand; 4 | 5 | /// 6 | /// Interface defining how to read a field value 7 | /// 8 | /// The type of the value. 9 | public interface IDataFieldConverter 10 | { 11 | /// 12 | /// Read the value from the specified . 13 | /// 14 | /// The data record to read the value from. 15 | /// Index of the field. 16 | /// The value read from the 17 | public TValue ReadValue(IDataRecord dataRecord, int fieldIndex); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/FluentCommand/IDataParameterHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand; 4 | 5 | 6 | /// 7 | /// An interface defining parameter type handling 8 | /// 9 | public interface IDataParameterHandler 10 | { 11 | /// 12 | /// Gets the type of the value to handle. 13 | /// 14 | /// 15 | /// The type of the value to handle. 16 | /// 17 | Type ValueType { get; } 18 | 19 | /// 20 | /// Read the value from the specified . 21 | /// 22 | /// The parameter to read the value from. 23 | /// The value read from the 24 | object ReadValue(IDbDataParameter parameter); 25 | 26 | /// 27 | /// Set the value on the specified . 28 | /// 29 | /// The parameter to be updated. 30 | /// The value to set. 31 | void SetValue(IDbDataParameter parameter, object value); 32 | } 33 | -------------------------------------------------------------------------------- /src/FluentCommand/IDataQueryFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand; 4 | 5 | /// 6 | /// A interface for formatting an for logging 7 | /// 8 | public interface IDataQueryFormatter 9 | { 10 | /// 11 | /// Formats the command for logging. 12 | /// 13 | /// The command to log. 14 | /// The execution duration. 15 | /// The exception thrown when executing the command. 16 | /// The command formatted as a string 17 | string FormatCommand(IDbCommand command, TimeSpan duration, Exception exception); 18 | } 19 | -------------------------------------------------------------------------------- /src/FluentCommand/IDataQueryLogger.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand; 4 | 5 | /// 6 | /// An interface for logging queries 7 | /// 8 | public interface IDataQueryLogger 9 | { 10 | /// 11 | /// Log the current specified 12 | /// 13 | /// The command to log. 14 | /// The execution duration. 15 | /// The exception thrown when executing the command. 16 | /// The state used to control logging. 17 | /// 18 | void LogCommand(IDbCommand command, TimeSpan duration, Exception exception = null, object state = null); 19 | } 20 | -------------------------------------------------------------------------------- /src/FluentCommand/IDataSessionFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | namespace FluentCommand; 4 | 5 | /// 6 | /// An interface for creating instances 7 | /// 8 | public interface IDataSessionFactory 9 | { 10 | /// 11 | /// Creates a new data session from this database configuration 12 | /// 13 | /// The connection string to use for the session 14 | /// 15 | /// A new instance. 16 | /// 17 | IDataSession CreateSession(string connectionString = null); 18 | 19 | /// 20 | /// Creates a new data session from this database configuration 21 | /// 22 | /// The transaction to create the session with. 23 | /// 24 | /// A new instance. 25 | /// 26 | IDataSession CreateSession(DbTransaction transaction); 27 | } 28 | 29 | /// 30 | /// The data session factory by discriminator. Used to register multiple instances of IDataSessionFactory. 31 | /// 32 | /// The type of the discriminator. 33 | public interface IDataSessionFactory : IDataSessionFactory 34 | { 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/FluentCommand/Internal/Internals.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("FluentCommand.Tests")] 4 | -------------------------------------------------------------------------------- /src/FluentCommand/Internal/Singleton.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace FluentCommand.Internal; 4 | 5 | /// 6 | /// A class representing a singleton pattern. 7 | /// 8 | /// The type of the singleton 9 | public static class Singleton 10 | where T : class, new() 11 | { 12 | private static readonly Lazy _instance = new(); 13 | 14 | /// 15 | /// Gets the current instance of the singleton. 16 | /// 17 | [DebuggerBrowsable(DebuggerBrowsableState.Never)] 18 | [DebuggerNonUserCode] 19 | public static T Current => _instance.Value; 20 | } 21 | -------------------------------------------------------------------------------- /src/FluentCommand/Internal/StringBuilderCache.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace FluentCommand.Internal; 4 | 5 | /// Provide a cached reusable instance of StringBuilder per thread. 6 | public static class StringBuilderCache 7 | { 8 | // The value 360 was chosen in discussion with performance experts as a compromise between using 9 | // as little memory per thread as possible and still covering a large part of short-lived 10 | // StringBuilder creations on the startup path of VS designers. 11 | internal const int MaxBuilderSize = 360; 12 | private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity 13 | 14 | [ThreadStatic] 15 | private static StringBuilder t_cachedInstance; 16 | 17 | /// Get a StringBuilder for the specified capacity. 18 | /// If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied. 19 | public static StringBuilder Acquire(int capacity = DefaultCapacity) 20 | { 21 | if (capacity > MaxBuilderSize) 22 | return new StringBuilder(capacity); 23 | 24 | var sb = t_cachedInstance; 25 | if (sb == null) 26 | return new StringBuilder(capacity); 27 | 28 | // Avoid StringBuilder block fragmentation by getting a new StringBuilder 29 | // when the requested size is larger than the current capacity 30 | if (capacity > sb.Capacity) 31 | return new StringBuilder(capacity); 32 | 33 | t_cachedInstance = null; 34 | sb.Clear(); 35 | 36 | return sb; 37 | 38 | } 39 | 40 | /// Place the specified builder in the cache if it is not too big. 41 | public static void Release(StringBuilder sb) 42 | { 43 | if (sb.Capacity <= MaxBuilderSize) 44 | t_cachedInstance = sb; 45 | } 46 | 47 | /// Release StringBuilder to the cache, and return the resulting string. 48 | public static string ToString(StringBuilder sb) 49 | { 50 | string result = sb.ToString(); 51 | Release(sb); 52 | return result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/AggregateFunctions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// Query aggregate functions 5 | /// 6 | public enum AggregateFunctions 7 | { 8 | /// Average aggregate function 9 | Average, 10 | /// Count aggregate function 11 | Count, 12 | /// Max aggregate function 13 | Max, 14 | /// Min aggregate function 15 | Min, 16 | /// Sum aggregate function 17 | Sum, 18 | } 19 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/FilterOperators.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// Query filter operators 5 | /// 6 | public enum FilterOperators 7 | { 8 | /// Starts with query operator 9 | StartsWith, 10 | /// Ends with query operator 11 | EndsWith, 12 | /// Contains query operator 13 | Contains, 14 | /// Equal query operator 15 | Equal, 16 | /// Not equal query operator 17 | NotEqual, 18 | /// Less than query operator 19 | LessThan, 20 | /// Less than or equal query operator 21 | LessThanOrEqual, 22 | /// Greater than query operator 23 | GreaterThan, 24 | /// Greater than or equal query operator 25 | GreaterThanOrEqual, 26 | /// Is null query operator 27 | IsNull, 28 | /// Is not null query operator 29 | IsNotNull, 30 | /// In query operator 31 | In 32 | } 33 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/Generators/IQueryGenerator.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query.Generators; 2 | 3 | public interface IQueryGenerator 4 | { 5 | string BuildDelete(DeleteStatement deleteStatement); 6 | 7 | string BuildInsert(InsertStatement insertStatement); 8 | 9 | string BuildSelect(SelectStatement selectStatement); 10 | 11 | string BuildUpdate(UpdateStatement updateStatement); 12 | 13 | 14 | string BuildWhere(IReadOnlyCollection whereExpressions); 15 | 16 | string BuildOrder(IReadOnlyCollection sortExpressions); 17 | 18 | 19 | string CommentExpression(string comment); 20 | 21 | string AggregateExpression(AggregateExpression aggregateExpression); 22 | 23 | string TableExpression(TableExpression tableExpression); 24 | 25 | string LimitExpression(LimitExpression limitExpression); 26 | 27 | string LogicalExpression(IReadOnlyCollection whereExpressions, LogicalOperators logicalOperator); 28 | 29 | string SortExpression(SortExpression sortExpression); 30 | 31 | string ColumnExpression(ColumnExpression columnExpression); 32 | 33 | string UpdateExpression(UpdateExpression updateExpression); 34 | 35 | string WhereExpression(WhereExpression whereExpression); 36 | 37 | string GroupExpression(GroupExpression groupExpression); 38 | 39 | string JoinExpression(JoinExpression joinExpression); 40 | } 41 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/IQueryBuilder.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Query.Generators; 2 | 3 | namespace FluentCommand.Query; 4 | 5 | /// 6 | /// Interface for query building 7 | /// 8 | public interface IQueryBuilder 9 | { 10 | /// 11 | /// Gets the query generator. 12 | /// 13 | /// 14 | /// The query generator. 15 | /// 16 | IQueryGenerator QueryGenerator { get; } 17 | 18 | /// 19 | /// Gets the query parameters. 20 | /// 21 | /// 22 | /// The query parameters. 23 | /// 24 | List Parameters { get; } 25 | } 26 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/IQueryStatement.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// interface for defining a sql query 5 | /// 6 | public interface IQueryStatement 7 | { 8 | /// 9 | /// Gets the parameters for the query. 10 | /// 11 | /// 12 | /// The parameters for the query. 13 | /// 14 | IReadOnlyCollection Parameters { get; } 15 | 16 | /// 17 | /// Gets the sql statement. 18 | /// 19 | /// 20 | /// The sql statement. 21 | /// 22 | string Statement { get; } 23 | } 24 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/IStatementBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// interface defining sql statement builder 5 | /// 6 | public interface IStatementBuilder 7 | { 8 | /// 9 | /// Builds the sql statement from this builder. 10 | /// 11 | /// The sql query statement 12 | QueryStatement BuildStatement(); 13 | } 14 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/JoinTypes.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// Query join types 5 | /// 6 | public enum JoinTypes 7 | { 8 | /// The inner join tables 9 | Inner, 10 | /// The left outer join tables 11 | Left, 12 | /// The right outer join tables 13 | Right 14 | } 15 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/LogicalBuilder.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Query.Generators; 2 | 3 | namespace FluentCommand.Query; 4 | 5 | /// 6 | /// A logical query expression builder 7 | /// 8 | public class LogicalBuilder : LogicalBuilder 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The query generator. 14 | /// The query parameters. 15 | /// The query logical operator. 16 | public LogicalBuilder( 17 | IQueryGenerator queryGenerator, 18 | List parameters, 19 | LogicalOperators logicalOperator = LogicalOperators.And) 20 | : base(queryGenerator, parameters, logicalOperator) 21 | { 22 | } 23 | } 24 | 25 | /// 26 | /// A logical query expression builder 27 | /// 28 | public abstract class LogicalBuilder : WhereBuilder 29 | where TBuilder : LogicalBuilder 30 | { 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The query generator. 35 | /// The query parameters. 36 | /// The query logical operator. 37 | protected LogicalBuilder( 38 | IQueryGenerator queryGenerator, 39 | List parameters, 40 | LogicalOperators logicalOperator = LogicalOperators.And) 41 | : base(queryGenerator, parameters, logicalOperator) 42 | { 43 | } 44 | 45 | /// 46 | public override QueryStatement BuildStatement() 47 | { 48 | if (WhereExpressions.Count == 0) 49 | return null; 50 | 51 | var statement = QueryGenerator.LogicalExpression(WhereExpressions, LogicalOperator); 52 | 53 | return new QueryStatement(statement, Parameters); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/LogicalOperators.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// Query logical operators 5 | /// 6 | public enum LogicalOperators 7 | { 8 | /// true if all Boolean expressions are true 9 | And, 10 | /// true if any Boolean expressions are true 11 | Or 12 | } 13 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/QueryBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// Extension methods for building a query for the data session 5 | /// 6 | public static class QueryBuilderExtensions 7 | { 8 | /// 9 | /// Set the data command statement using the builder action. 10 | /// 11 | /// The data session. 12 | /// The query builder action. 13 | /// 14 | /// A fluent to a data command. 15 | /// 16 | public static IDataCommand Sql(this IDataSession dataSession, Action builder) 17 | { 18 | var queryParameters = new List(); 19 | var queryBuilder = new QueryBuilder(dataSession.QueryGenerator, queryParameters); 20 | 21 | builder(queryBuilder); 22 | 23 | var statement = queryBuilder.BuildStatement(); 24 | 25 | var dataCommand = dataSession.Sql(statement?.Statement ?? string.Empty); 26 | 27 | if (statement?.Parameters == null) 28 | return dataCommand; 29 | 30 | foreach (var parameter in statement.Parameters) 31 | dataCommand.Parameter(parameter.Name, parameter.Value, parameter.Type); 32 | 33 | return dataCommand; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/QueryParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FluentCommand.Query; 4 | 5 | /// 6 | /// A query parameter 7 | /// 8 | public class QueryParameter 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The parameter name. 14 | /// The parameter value. 15 | /// The parameter type. 16 | public QueryParameter(string name, object value, Type type) 17 | { 18 | Name = name; 19 | Value = value; 20 | Type = type; 21 | } 22 | 23 | /// 24 | /// Gets the parameter name. 25 | /// 26 | /// 27 | /// The parameter name. 28 | /// 29 | public string Name { get; } 30 | 31 | /// 32 | /// Gets the parameter value. 33 | /// 34 | /// 35 | /// The parameter value. 36 | /// 37 | public object Value { get; } 38 | 39 | /// 40 | /// Gets the parameter type. 41 | /// 42 | /// 43 | /// The parameter type. 44 | /// 45 | public Type Type { get; } 46 | } 47 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/QueryStatement.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// class defining a sql query 5 | /// 6 | /// 7 | public class QueryStatement : IQueryStatement 8 | { 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The sql statement. 13 | /// The query parameters. 14 | /// statement cannot be null or whitespace. - statement 15 | /// parameters cannot be null 16 | public QueryStatement(string statement, IReadOnlyCollection parameters) 17 | { 18 | if (string.IsNullOrWhiteSpace(statement)) 19 | throw new ArgumentException($"'{nameof(statement)}' cannot be null or whitespace.", nameof(statement)); 20 | 21 | if (parameters is null) 22 | throw new ArgumentNullException(nameof(parameters)); 23 | 24 | Statement = statement; 25 | Parameters = parameters; 26 | } 27 | 28 | /// 29 | public string Statement { get; } 30 | 31 | /// 32 | public IReadOnlyCollection Parameters { get; } 33 | } 34 | -------------------------------------------------------------------------------- /src/FluentCommand/Query/SortDirections.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Query; 2 | 3 | /// 4 | /// Query sort directions 5 | /// 6 | public enum SortDirections 7 | { 8 | /// Sort ascending direction 9 | Ascending, 10 | /// Sort descending direction 11 | Descending 12 | } 13 | -------------------------------------------------------------------------------- /src/FluentCommand/Reflection/IMemberAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Reflection; 2 | 3 | /// 4 | /// An for late binding member accessors. 5 | /// 6 | public interface IMemberAccessor : IMemberInformation 7 | { 8 | /// 9 | /// Returns the value of the member. 10 | /// 11 | /// The instance whose member value will be returned. 12 | /// The member value for the instance parameter. 13 | object GetValue(object instance); 14 | 15 | /// 16 | /// Sets the of the member. 17 | /// 18 | /// The instance whose member value will be set. 19 | /// The new value for this member. 20 | void SetValue(object instance, object value); 21 | } 22 | -------------------------------------------------------------------------------- /src/FluentCommand/Reflection/IMethodAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace FluentCommand.Reflection; 4 | 5 | /// 6 | /// An interface for method accessor 7 | /// 8 | public interface IMethodAccessor 9 | { 10 | /// 11 | /// Gets the method info. 12 | /// 13 | MethodInfo MethodInfo { get; } 14 | 15 | /// 16 | /// Gets the name of the member. 17 | /// 18 | /// The name of the member. 19 | string Name { get; } 20 | 21 | /// 22 | /// Invokes the method on the specified . 23 | /// 24 | /// The object on which to invoke the method. If a method is static, this argument is ignored. 25 | /// An argument list for the invoked method. 26 | /// An object containing the return value of the invoked method. 27 | object Invoke(object instance, params object[] arguments); 28 | } 29 | -------------------------------------------------------------------------------- /src/FluentCommand/Reflection/MethodAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Reflection; 3 | 4 | namespace FluentCommand.Reflection; 5 | 6 | /// 7 | /// An accessor class for . 8 | /// 9 | [DebuggerDisplay("Name: {Name}")] 10 | public class MethodAccessor : IMethodAccessor 11 | { 12 | private readonly Lazy> _invoker; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The method info. 18 | public MethodAccessor(MethodInfo methodInfo) 19 | { 20 | if (methodInfo == null) 21 | throw new ArgumentNullException(nameof(methodInfo)); 22 | 23 | MethodInfo = methodInfo; 24 | Name = methodInfo.Name; 25 | _invoker = new Lazy>(() => ExpressionFactory.CreateMethod(MethodInfo)); 26 | } 27 | 28 | /// 29 | /// Gets the method info. 30 | /// 31 | public MethodInfo MethodInfo { get; } 32 | 33 | /// 34 | /// Gets the name of the member. 35 | /// 36 | /// 37 | /// The name of the member. 38 | /// 39 | public string Name { get; } 40 | 41 | /// 42 | /// Invokes the method on the specified . 43 | /// 44 | /// The object on which to invoke the method. If a method is static, this argument is ignored. 45 | /// An argument list for the invoked method. 46 | /// 47 | /// An object containing the return value of the invoked method. 48 | /// 49 | public object Invoke(object instance, params object[] arguments) 50 | { 51 | return _invoker.Value.Invoke(instance, arguments); 52 | } 53 | 54 | /// 55 | /// Gets the method key using a hash code from the name and paremeter types. 56 | /// 57 | /// The name of the method. 58 | /// The method parameter types. 59 | /// The method key 60 | internal static int GetKey(string name, IEnumerable parameterTypes) 61 | { 62 | return Internal.HashCode.Seed 63 | .Combine(name) 64 | .CombineAll(parameterTypes); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft 5 | LoreSoft 6 | en-US 7 | false 8 | 9 | 10 | 11 | embedded 12 | true 13 | false 14 | 15 | 16 | 17 | latest 18 | enable 19 | 1591 20 | 21 | 22 | 23 | v 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/FluentCommand.Batch.Tests/FluentCommand.Batch.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | false 6 | 7 | 8 | 9 | 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | all 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Always 34 | 35 | 36 | Always 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test/FluentCommand.Batch.Tests/UserProfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DataGenerator; 4 | using DataGenerator.Sources; 5 | 6 | using FluentCommand.Entities; 7 | 8 | namespace FluentCommand.Batch.Tests; 9 | 10 | public class UserProfile : MappingProfile 11 | { 12 | public override void Configure() 13 | { 14 | Property(p => p.FirstName).DataSource(); 15 | Property(p => p.LastName).DataSource(); 16 | Property(p => p.DisplayName).Value(u => $"{u.FirstName} {u.LastName}"); 17 | Property(p => p.EmailAddress).Value(u => $"{u.FirstName}.{u.LastName}.{DateTime.Now.Ticks}@mailinator.com"); 18 | Property(p => p.IsEmailAddressConfirmed).Value(true); 19 | Property(p => p.PasswordHash).DataSource(); 20 | Property(p => p.IsDeleted).Value(false); 21 | Property(p => p.LockoutEnabled).Value(false); 22 | Property(p => p.AccessFailedCount).Value(0); 23 | 24 | 25 | Property(p => p.Created).Value(u => DateTimeOffset.UtcNow); 26 | Property(p => p.CreatedBy).Value("UnitTest"); 27 | Property(p => p.Updated).Value(u => DateTimeOffset.UtcNow); 28 | Property(p => p.UpdatedBy).Value("UnitTest"); 29 | 30 | Property(p => p.RowVersion).Ignore(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/FluentCommand.Batch.Tests/appsettings.appveyor.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=(local)\\SQL2019;Database=TrackerAppVeyor;Integrated security=SSPI;TrustServerCertificate=True;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Batch.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Data Source=(local)\\SQL2019;Initial Catalog=TrackerTest;Integrated Security=True;TrustServerCertificate=True;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Audit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | using FluentCommand.Handlers; 6 | 7 | namespace FluentCommand.Entities; 8 | 9 | [Table(nameof(Audit))] 10 | public class Audit 11 | { 12 | public int Id { get; set; } 13 | public DateTime Date { get; set; } 14 | public int? UserId { get; set; } 15 | public int? TaskId { get; set; } 16 | public string Content { get; set; } 17 | public string Username { get; set; } 18 | public DateTimeOffset Created { get; set; } 19 | public string CreatedBy { get; set; } 20 | public DateTimeOffset Updated { get; set; } 21 | public string UpdatedBy { get; set; } 22 | 23 | [ConcurrencyCheck] 24 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 25 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 26 | public ConcurrencyToken RowVersion { get; set; } 27 | 28 | [NotMapped] 29 | public virtual Task Task { get; set; } 30 | [NotMapped] 31 | public virtual User User { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Brand.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Entities; 2 | 3 | public class Brand 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public string Description { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/DataType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace FluentCommand.Entities; 6 | 7 | [Table("DataType", Schema = "dbo")] 8 | public class DataType 9 | { 10 | [Key] 11 | public long Id { get; set; } 12 | 13 | public string Name { get; set; } 14 | 15 | public bool Boolean { get; set; } 16 | 17 | public short Short { get; set; } 18 | 19 | public long Long { get; set; } 20 | 21 | public float Float { get; set; } 22 | 23 | public double Double { get; set; } 24 | 25 | public decimal Decimal { get; set; } 26 | 27 | public DateTime DateTime { get; set; } 28 | 29 | public DateTimeOffset DateTimeOffset { get; set; } 30 | 31 | public Guid Guid { get; set; } 32 | 33 | public TimeSpan TimeSpan { get; set; } 34 | 35 | #if NET6_0_OR_GREATER 36 | public DateOnly DateOnly { get; set; } 37 | 38 | public TimeOnly TimeOnly { get; set; } 39 | #endif 40 | 41 | public bool? BooleanNull { get; set; } 42 | 43 | public short? ShortNull { get; set; } 44 | 45 | public long? LongNull { get; set; } 46 | 47 | public float? FloatNull { get; set; } 48 | 49 | public double? DoubleNull { get; set; } 50 | 51 | public decimal? DecimalNull { get; set; } 52 | 53 | public DateTime? DateTimeNull { get; set; } 54 | 55 | public DateTimeOffset? DateTimeOffsetNull { get; set; } 56 | 57 | public Guid? GuidNull { get; set; } 58 | 59 | public TimeSpan? TimeSpanNull { get; set; } 60 | 61 | #if NET6_0_OR_GREATER 62 | public DateOnly? DateOnlyNull { get; set; } 63 | 64 | public TimeOnly? TimeOnlyNull { get; set; } 65 | #endif 66 | } 67 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/FluentCommand.Entities.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net8.0;net9.0 5 | false 6 | latest 7 | true 8 | 9 | 10 | 11 | 12 | Analyzer 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Internal/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | #if NETSTANDARD2_0 2 | using System.ComponentModel; 3 | 4 | namespace System.Runtime.CompilerServices; 5 | 6 | [EditorBrowsable(EditorBrowsableState.Never)] 7 | sealed class IsExternalInit { } 8 | #endif 9 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Member.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace FluentCommand.Entities; 4 | 5 | [Table("member_user", Schema = "dbo")] 6 | public class Member 7 | { 8 | [Column("Id")] 9 | public Guid Id { get; set; } 10 | 11 | [Column("email_address")] 12 | public string EmailAddress { get; set; } 13 | 14 | [Column("display_name")] 15 | public string DisplayName { get; set; } 16 | 17 | [Column("first_name")] 18 | public string FirstName { get; set; } 19 | 20 | [Column("last_name")] 21 | public string LastName { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Priority.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | using FluentCommand.Handlers; 7 | 8 | namespace FluentCommand.Entities; 9 | 10 | [Table(nameof(Priority))] 11 | public class Priority 12 | { 13 | public int Id { get; set; } 14 | public string Name { get; set; } 15 | public string Description { get; set; } 16 | public int DisplayOrder { get; set; } 17 | public bool IsActive { get; set; } 18 | public DateTimeOffset Created { get; set; } 19 | public string CreatedBy { get; set; } 20 | public DateTimeOffset Updated { get; set; } 21 | public string UpdatedBy { get; set; } 22 | 23 | [ConcurrencyCheck] 24 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 25 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 26 | public ConcurrencyToken RowVersion { get; set; } 27 | 28 | [NotMapped] 29 | public virtual ICollection Tasks { get; set; } = new List(); 30 | } 31 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Product.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.Entities; 2 | 3 | public class Product 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } 7 | public string Description { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/ReaderGeneration.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Attributes; 2 | using FluentCommand.Entities; 3 | 4 | [assembly: GenerateReader(typeof(Brand))] 5 | [assembly: GenerateReader(typeof(Product))] 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Role.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | using FluentCommand.Handlers; 7 | 8 | namespace FluentCommand.Entities; 9 | 10 | [Table(nameof(Role))] 11 | public class Role 12 | { 13 | public Guid Id { get; set; } 14 | public string Name { get; set; } 15 | public string Description { get; set; } 16 | public DateTimeOffset Created { get; set; } 17 | public string CreatedBy { get; set; } 18 | public DateTimeOffset Updated { get; set; } 19 | public string UpdatedBy { get; set; } 20 | 21 | [ConcurrencyCheck] 22 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 23 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 24 | public ConcurrencyToken RowVersion { get; set; } 25 | 26 | [NotMapped] 27 | public virtual ICollection Users { get; set; } = new List(); 28 | } 29 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Status.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | using FluentCommand.Handlers; 7 | 8 | namespace FluentCommand.Entities; 9 | 10 | [Table("Status", Schema = "dbo")] 11 | public class Status 12 | { 13 | public int Id { get; set; } 14 | public string Name { get; set; } 15 | public string Description { get; set; } 16 | public int DisplayOrder { get; set; } 17 | public bool IsActive { get; set; } 18 | public DateTimeOffset Created { get; set; } 19 | public string CreatedBy { get; set; } 20 | public DateTimeOffset Updated { get; set; } 21 | public string UpdatedBy { get; set; } 22 | 23 | [ConcurrencyCheck] 24 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 25 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 26 | public ConcurrencyToken RowVersion { get; set; } 27 | 28 | [NotMapped] 29 | public virtual ICollection Tasks { get; set; } = new List(); 30 | } 31 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/StatusConstructor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | using FluentCommand.Handlers; 6 | 7 | namespace FluentCommand.Entities; 8 | 9 | [Table(nameof(StatusConstructor))] 10 | public class StatusConstructor 11 | { 12 | public StatusConstructor( 13 | int id, 14 | string name, 15 | string description, 16 | bool isActive, 17 | int displayOrder, 18 | DateTimeOffset created, 19 | string createdBy, 20 | DateTimeOffset updated, 21 | string updatedBy, 22 | ConcurrencyToken rowVersion) 23 | { 24 | Id = id; 25 | Name = name; 26 | Description = description; 27 | DisplayOrder = displayOrder; 28 | IsActive = isActive; 29 | Created = created; 30 | CreatedBy = createdBy; 31 | Updated = updated; 32 | UpdatedBy = updatedBy; 33 | RowVersion = rowVersion; 34 | } 35 | 36 | public int Id { get; } 37 | public string Name { get; } 38 | public string Description { get; } 39 | public int DisplayOrder { get; } 40 | public bool IsActive { get; } 41 | public DateTimeOffset Created { get; } 42 | public string CreatedBy { get; } 43 | public DateTimeOffset Updated { get; } 44 | public string UpdatedBy { get; } 45 | 46 | [ConcurrencyCheck] 47 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 48 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 49 | public ConcurrencyToken RowVersion { get; } 50 | } 51 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/StatusReadOnly.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | using FluentCommand.Handlers; 6 | 7 | namespace FluentCommand.Entities; 8 | 9 | [Table(nameof(StatusReadOnly))] 10 | public class StatusReadOnly 11 | { 12 | public int Id { get; init; } 13 | public string Name { get; init; } 14 | public string Description { get; init; } 15 | public int DisplayOrder { get; init; } 16 | public bool IsActive { get; init; } 17 | public DateTimeOffset Created { get; init; } 18 | public string CreatedBy { get; init; } 19 | public DateTimeOffset Updated { get; init; } 20 | public string UpdatedBy { get; init; } 21 | 22 | [ConcurrencyCheck] 23 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 24 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 25 | public ConcurrencyToken RowVersion { get; init; } 26 | } 27 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/StatusRecord.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | using FluentCommand.Handlers; 5 | 6 | namespace FluentCommand.Entities; 7 | 8 | [Table(nameof(StatusRecord))] 9 | public record StatusRecord( 10 | int Id, 11 | string Name, 12 | string Description, 13 | int DisplayOrder, 14 | bool IsActive, 15 | DateTimeOffset Created, 16 | string CreatedBy, 17 | DateTimeOffset Updated, 18 | string UpdatedBy, 19 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 20 | ConcurrencyToken RowVersion 21 | ); 22 | 23 | public record StatusRecordList( 24 | int Id, 25 | string Name, 26 | string Description, 27 | int DisplayOrder, 28 | bool IsActive, 29 | DateTimeOffset Created, 30 | string CreatedBy, 31 | DateTimeOffset Updated, 32 | string UpdatedBy, 33 | ConcurrencyToken RowVersion, 34 | List Versions 35 | ); 36 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/TableTest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace FluentCommand.Entities; 5 | 6 | [Table("Table1 $ Test", Schema = "dbo")] 7 | public class TableTest 8 | { 9 | private const string BlahColumn = "Blah #"; 10 | 11 | [Key] 12 | [Column("Test$")] 13 | public string Test { get; set; } = null!; 14 | 15 | [Column(BlahColumn)] 16 | public string Blah { get; set; } 17 | 18 | [Column("Table Example ID")] 19 | public int? TableExampleID { get; set; } 20 | 21 | public int? TableExampleObject { get; set; } 22 | 23 | [Column("1stNumber")] 24 | public string FirstNumber { get; set; } 25 | 26 | [Column("123Street")] 27 | public string Street { get; set; } 28 | 29 | [Column("123 Test 123")] 30 | public string Test123 { get; set; } 31 | } 32 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/Task.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | using FluentCommand.Handlers; 7 | 8 | namespace FluentCommand.Entities; 9 | 10 | [Table(nameof(Task))] 11 | public class Task 12 | { 13 | public Guid Id { get; set; } 14 | public int StatusId { get; set; } 15 | public int? PriorityId { get; set; } 16 | public string Title { get; set; } 17 | public string Description { get; set; } 18 | public DateTimeOffset? StartDate { get; set; } 19 | public DateTimeOffset? DueDate { get; set; } 20 | public DateTimeOffset? CompleteDate { get; set; } 21 | public Guid? AssignedId { get; set; } 22 | public DateTimeOffset Created { get; set; } 23 | public string CreatedBy { get; set; } 24 | public DateTimeOffset Updated { get; set; } 25 | public string UpdatedBy { get; set; } 26 | 27 | [ConcurrencyCheck] 28 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 29 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 30 | public ConcurrencyToken RowVersion { get; set; } 31 | 32 | [NotMapped] 33 | public virtual ICollection Audits { get; set; } = new List(); 34 | [NotMapped] 35 | public virtual Priority Priority { get; set; } 36 | [NotMapped] 37 | public virtual Status Status { get; set; } 38 | [NotMapped] 39 | public virtual User AssignedUser { get; set; } 40 | [NotMapped] 41 | public virtual User CreatedUser { get; set; } 42 | [NotMapped] 43 | public virtual TaskExtended TaskExtended { get; set; } 44 | } 45 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/TaskExtended.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace FluentCommand.Entities; 6 | 7 | [Table(nameof(TaskExtended))] 8 | public class TaskExtended 9 | { 10 | public Guid TaskId { get; set; } 11 | public string UserAgent { get; set; } 12 | public string Browser { get; set; } 13 | public string OperatingSystem { get; set; } 14 | public DateTimeOffset Created { get; set; } 15 | public string CreatedBy { get; set; } 16 | public DateTimeOffset Updated { get; set; } 17 | public string UpdatedBy { get; set; } 18 | 19 | [ConcurrencyCheck] 20 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 21 | public ConcurrencyToken RowVersion { get; set; } 22 | 23 | [NotMapped] 24 | public virtual Task Task { get; set; } 25 | } 26 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | using FluentCommand.Handlers; 7 | 8 | namespace FluentCommand.Entities; 9 | 10 | [Table(nameof(User))] 11 | public class User 12 | { 13 | public Guid Id { get; set; } 14 | public string EmailAddress { get; set; } 15 | public bool IsEmailAddressConfirmed { get; set; } 16 | public string DisplayName { get; set; } 17 | public string FirstName { get; set; } 18 | public string LastName { get; set; } 19 | public string PasswordHash { get; set; } 20 | public string ResetHash { get; set; } 21 | public string InviteHash { get; set; } 22 | public int AccessFailedCount { get; set; } 23 | public bool LockoutEnabled { get; set; } 24 | public DateTimeOffset? LockoutEnd { get; set; } 25 | public DateTimeOffset? LastLogin { get; set; } 26 | public bool IsDeleted { get; set; } 27 | public DateTimeOffset Created { get; set; } 28 | public string CreatedBy { get; set; } 29 | public DateTimeOffset Updated { get; set; } 30 | public string UpdatedBy { get; set; } 31 | 32 | [ConcurrencyCheck] 33 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 34 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 35 | public ConcurrencyToken RowVersion { get; set; } 36 | 37 | 38 | [NotMapped] 39 | public virtual ICollection Audits { get; set; } = new List(); 40 | 41 | [NotMapped] 42 | public virtual ICollection AssignedTasks { get; set; } = new List(); 43 | 44 | [NotMapped] 45 | public virtual ICollection CreatedTasks { get; set; } = new List(); 46 | 47 | [NotMapped] 48 | public virtual ICollection Roles { get; set; } = new List(); 49 | 50 | } 51 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/UserImport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace FluentCommand.Entities; 5 | 6 | [Table("User")] 7 | public class UserImport 8 | { 9 | public string EmailAddress { get; set; } 10 | public string DisplayName { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public DateTimeOffset? LockoutEnd { get; set; } 14 | public DateTimeOffset? LastLogin { get; set; } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/UserLogin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace FluentCommand.Entities; 6 | 7 | [Table(nameof(UserLogin))] 8 | public class UserLogin 9 | { 10 | public Guid Id { get; set; } 11 | public string EmailAddress { get; set; } 12 | public Guid? UserId { get; set; } 13 | public string UserAgent { get; set; } 14 | public string Browser { get; set; } 15 | public string OperatingSystem { get; set; } 16 | public string DeviceFamily { get; set; } 17 | public string DeviceBrand { get; set; } 18 | public string DeviceModel { get; set; } 19 | public string IpAddress { get; set; } 20 | public bool IsSuccessful { get; set; } 21 | public string FailureMessage { get; set; } 22 | public DateTimeOffset Created { get; set; } 23 | public string CreatedBy { get; set; } 24 | public DateTimeOffset Updated { get; set; } 25 | public string UpdatedBy { get; set; } 26 | 27 | [ConcurrencyCheck] 28 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 29 | public ConcurrencyToken RowVersion { get; set; } 30 | 31 | [NotMapped] 32 | public virtual User User { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /test/FluentCommand.Entities/UserRole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace FluentCommand.Entities; 5 | 6 | [Table(nameof(UserRole))] 7 | public class UserRole 8 | { 9 | public Guid UserId { get; set; } 10 | public Guid RoleId { get; set; } 11 | 12 | [NotMapped] 13 | public virtual User User { get; set; } 14 | [NotMapped] 15 | public virtual Role Role { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /test/FluentCommand.Generators.Tests/DataReaderFactoryWriterTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | using FluentCommand.Generators.Models; 4 | 5 | namespace FluentCommand.Generators.Tests; 6 | 7 | public class DataReaderFactoryWriterTests 8 | { 9 | [Fact] 10 | public async Task Generate() 11 | { 12 | var entityClass = new EntityClass( 13 | InitializationMode.ObjectInitializer, 14 | "global::FluentCommand.Entities.Status", 15 | "FluentCommand.Entities", 16 | "Status", 17 | new EntityProperty[] 18 | { 19 | new("Id", "Id", typeof(int).FullName), 20 | new("Name", "Name", typeof(string).FullName), 21 | new("IsActive", "IsActive", typeof(bool).FullName), 22 | new("Updated", "Updated", typeof(DateTimeOffset).FullName), 23 | new("RowVersion", "RowVersion", typeof(byte[]).FullName), 24 | } 25 | ); 26 | 27 | var source = DataReaderFactoryWriter.Generate(entityClass); 28 | 29 | await Verifier 30 | .Verify(source) 31 | .UseDirectory("Snapshots") 32 | .ScrubLinesContaining("GeneratedCodeAttribute"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/FluentCommand.Generators.Tests/FluentCommand.Generators.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/FluentCommand.Performance/BenchmarkBase.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | 3 | using Microsoft.Data.SqlClient; 4 | 5 | 6 | namespace FluentCommand.Performance; 7 | 8 | [BenchmarkCategory("ORM")] 9 | public abstract class BenchmarkBase 10 | { 11 | private SqlConnection _connection; 12 | protected SqlConnection Connection => _connection; 13 | 14 | public static string ConnectionString { get; } = "Data Source=(local);Initial Catalog=tempdb;Integrated Security=True;TrustServerCertificate=True"; 15 | 16 | private int _index; 17 | protected int Index => _index; 18 | 19 | protected void BaseSetup() 20 | { 21 | _index = 0; 22 | 23 | _connection = new SqlConnection(ConnectionString); 24 | _connection.Open(); 25 | } 26 | 27 | protected int NextIndex() 28 | { 29 | _index++; 30 | if (_index >= 5000) 31 | _index = 1; 32 | 33 | return _index; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/FluentCommand.Performance/DapperBenchmarks.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Linq; 3 | 4 | using BenchmarkDotNet.Attributes; 5 | 6 | using Dapper; 7 | 8 | using FluentCommand.Entities; 9 | 10 | namespace FluentCommand.Performance; 11 | 12 | [Description("Dapper")] 13 | public class DapperBenchmarks : BenchmarkBase 14 | { 15 | [GlobalSetup] 16 | public void Setup() 17 | { 18 | BaseSetup(); 19 | } 20 | 21 | [Benchmark(Description = "Query (buffered)")] 22 | public Post QueryBuffered() 23 | { 24 | var i = NextIndex(); 25 | return Connection.Query("select * from Post where Id = @Id", new { Id = i }, buffered: true).First(); 26 | } 27 | 28 | [Benchmark(Description = "Query (unbuffered)")] 29 | public Post QueryUnbuffered() 30 | { 31 | var i = NextIndex(); 32 | return Connection.Query("select * from Post where Id = @Id", new { Id = i }, buffered: false).First(); 33 | } 34 | 35 | [Benchmark(Description = "QueryFirstOrDefault")] 36 | public Post QueryFirstOrDefault() 37 | { 38 | var i = NextIndex(); 39 | return Connection.QueryFirstOrDefault("select * from Post where Id = @Id", new { Id = i }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/FluentCommand.Performance/FluentCommand.Performance.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Analyzer 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/FluentCommand.Performance/Post.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace FluentCommand.Performance; 5 | 6 | [Table(nameof(Post))] 7 | public class Post 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Text { get; set; } 12 | 13 | public DateTime CreationDate { get; set; } 14 | 15 | public DateTime LastChangeDate { get; set; } 16 | 17 | public int? Counter1 { get; set; } 18 | 19 | public int? Counter2 { get; set; } 20 | 21 | public int? Counter3 { get; set; } 22 | 23 | public int? Counter4 { get; set; } 24 | 25 | public int? Counter5 { get; set; } 26 | 27 | public int? Counter6 { get; set; } 28 | 29 | public int? Counter7 { get; set; } 30 | 31 | public int? Counter8 { get; set; } 32 | 33 | public int? Counter9 { get; set; } 34 | } 35 | -------------------------------------------------------------------------------- /test/FluentCommand.Performance/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Configs; 2 | using BenchmarkDotNet.Diagnosers; 3 | using BenchmarkDotNet.Exporters; 4 | using BenchmarkDotNet.Exporters.Csv; 5 | using BenchmarkDotNet.Jobs; 6 | using BenchmarkDotNet.Order; 7 | using BenchmarkDotNet.Running; 8 | 9 | using Microsoft.Data.SqlClient; 10 | 11 | using static System.Console; 12 | 13 | namespace FluentCommand.Performance; 14 | 15 | internal static class Program 16 | { 17 | static void Main(string[] args) 18 | { 19 | WriteLine("Using ConnectionString: " + BenchmarkBase.ConnectionString); 20 | EnsureDatabase(); 21 | WriteLine("Database setup complete."); 22 | 23 | var benchmarkSwitcher = new BenchmarkSwitcher(typeof(BenchmarkBase).Assembly); 24 | 25 | var config = ManualConfig.CreateMinimumViable() 26 | .AddExporter(CsvExporter.Default) 27 | .AddExporter(MarkdownExporter.GitHub) 28 | .AddDiagnoser(MemoryDiagnoser.Default) 29 | .AddJob(Job.ShortRun 30 | .WithLaunchCount(1) 31 | .WithWarmupCount(2) 32 | .WithUnrollFactor(500) 33 | .WithIterationCount(10) 34 | ) 35 | .WithOption(ConfigOptions.JoinSummary, true) 36 | .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest)); 37 | 38 | benchmarkSwitcher.Run(args, config); 39 | } 40 | 41 | private static void EnsureDatabase() 42 | { 43 | using var connection = new SqlConnection(BenchmarkBase.ConnectionString); 44 | connection.Open(); 45 | 46 | using var command = connection.CreateCommand(); 47 | command.CommandText = @" 48 | If (Object_Id('Post') Is Null) 49 | Begin 50 | Create Table Post 51 | ( 52 | Id int identity primary key, 53 | [Text] varchar(max) not null, 54 | CreationDate datetime not null, 55 | LastChangeDate datetime not null, 56 | Counter1 int, 57 | Counter2 int, 58 | Counter3 int, 59 | Counter4 int, 60 | Counter5 int, 61 | Counter6 int, 62 | Counter7 int, 63 | Counter8 int, 64 | Counter9 int 65 | ); 66 | 67 | Set NoCount On; 68 | Declare @i int = 0; 69 | 70 | While @i <= 5001 71 | Begin 72 | Insert Post ([Text],CreationDate, LastChangeDate) values (replicate('x', 2000), GETDATE(), GETDATE()); 73 | Set @i = @i + 1; 74 | End 75 | End 76 | "; 77 | command.Connection = connection; 78 | command.ExecuteNonQuery(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/FluentCommand.Performance/PropertyAccessorBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Jobs; 6 | 7 | using FluentCommand.Entities; 8 | using FluentCommand.Reflection; 9 | 10 | namespace FluentCommand.Performance; 11 | 12 | [MemoryDiagnoser] 13 | [SimpleJob(RuntimeMoniker.Net70)] 14 | [SimpleJob(RuntimeMoniker.Net60)] 15 | [BenchmarkCategory("Accessor")] 16 | public class PropertyAccessorBenchmark 17 | { 18 | private readonly User _user; 19 | 20 | public PropertyAccessorBenchmark() 21 | { 22 | _user = new User 23 | { 24 | FirstName = "Test", 25 | LastName = "User", 26 | DisplayName = "Test User", 27 | EmailAddress = "test@email.com" 28 | }; 29 | } 30 | 31 | private PropertyInfo _propertyInfo; 32 | private IMemberAccessor _propertyAccessor; 33 | private Func _functionAccessor; 34 | 35 | [GlobalSetup] 36 | public void Setup() 37 | { 38 | var type = _user.GetType(); 39 | _propertyInfo = type.GetProperty(nameof(User.DisplayName)); 40 | 41 | var typeAccessor = TypeAccessor.GetAccessor(); 42 | _propertyAccessor = typeAccessor.FindProperty(nameof(User.DisplayName)); 43 | _functionAccessor = (p) => (p as User)?.DisplayName; 44 | 45 | } 46 | 47 | [Benchmark(Description = "ProperyRead", Baseline = true)] 48 | public string ProperyRead() 49 | { 50 | var displayName = _user.DisplayName; 51 | 52 | return displayName; 53 | } 54 | 55 | [Benchmark(Description = "ProperyReflection")] 56 | public string ProperyReflection() 57 | { 58 | var displayName = _propertyInfo.GetValue(_user) as string; 59 | 60 | return displayName; 61 | } 62 | 63 | [Benchmark(Description = "ProperyFunction")] 64 | public string ProperyFunction() 65 | { 66 | var displayName = _functionAccessor(_user) as string; 67 | return displayName; 68 | } 69 | 70 | [Benchmark(Description = "ProperyAccessor")] 71 | public string ProperyAccessor() 72 | { 73 | var displayName = _propertyAccessor.GetValue(_user) as string; 74 | return displayName; 75 | } 76 | 77 | 78 | } 79 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/DatabaseCollection.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.PostgreSQL.Tests; 2 | 3 | [CollectionDefinition(DatabaseCollection.CollectionName)] 4 | public class DatabaseCollection : ICollectionFixture 5 | { 6 | public const string CollectionName = "DatabaseCollection"; 7 | } 8 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/DatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | using Npgsql; 5 | 6 | using Testcontainers.PostgreSql; 7 | 8 | using XUnit.Hosting; 9 | 10 | namespace FluentCommand.PostgreSQL.Tests; 11 | 12 | public class DatabaseFixture : TestApplicationFixture, IAsyncLifetime 13 | { 14 | private readonly PostgreSqlContainer _postgreSqlContainer = new PostgreSqlBuilder() 15 | .WithDatabase("TrackerDocker") 16 | .Build(); 17 | 18 | public async Task InitializeAsync() 19 | { 20 | await _postgreSqlContainer.StartAsync(); 21 | } 22 | 23 | public async Task DisposeAsync() 24 | { 25 | await _postgreSqlContainer.DisposeAsync(); 26 | } 27 | 28 | protected override void ConfigureApplication(HostApplicationBuilder builder) 29 | { 30 | base.ConfigureApplication(builder); 31 | 32 | var services = builder.Services; 33 | var trackerConnection = _postgreSqlContainer.GetConnectionString(); 34 | 35 | services.AddHostedService(); 36 | 37 | services.AddFluentCommand(builder => builder 38 | .UseConnectionString(trackerConnection) 39 | .AddProviderFactory(NpgsqlFactory.Instance) 40 | .AddPostgreSqlGenerator() 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/DatabaseInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using DbUp; 4 | using DbUp.Engine.Output; 5 | 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace FluentCommand.PostgreSQL.Tests; 10 | 11 | public class DatabaseInitializer : IHostedService, IUpgradeLog 12 | { 13 | private readonly ILogger _logger; 14 | private readonly IDataConfiguration _configuration; 15 | 16 | public DatabaseInitializer(ILogger logger, IDataConfiguration configuration) 17 | { 18 | _logger = logger; 19 | _configuration = configuration; 20 | } 21 | 22 | 23 | public Task StartAsync(CancellationToken cancellationToken) 24 | { 25 | var connectionString = _configuration.ConnectionString; 26 | 27 | EnsureDatabase.For.PostgresqlDatabase(connectionString, this); 28 | 29 | var upgradeEngine = DeployChanges.To 30 | .PostgresqlDatabase(connectionString) 31 | .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) 32 | .LogTo(this) 33 | .Build(); 34 | 35 | var result = upgradeEngine.PerformUpgrade(); 36 | 37 | return result.Successful 38 | ? Task.CompletedTask 39 | : Task.FromException(result.Error); 40 | } 41 | 42 | public Task StopAsync(CancellationToken cancellationToken) 43 | { 44 | return Task.CompletedTask; 45 | } 46 | 47 | 48 | public void LogDebug(string format, params object[] args) 49 | { 50 | _logger.LogDebug(format, args); 51 | } 52 | 53 | public void LogError(string format, params object[] args) 54 | { 55 | _logger.LogError(format, args); 56 | } 57 | 58 | public void LogError(Exception ex, string format, params object[] args) 59 | { 60 | _logger.LogError(ex, format, args); 61 | } 62 | 63 | public void LogInformation(string format, params object[] args) 64 | { 65 | _logger.LogInformation(format, args); 66 | } 67 | 68 | public void LogTrace(string format, params object[] args) 69 | { 70 | _logger.LogTrace(format, args); 71 | } 72 | 73 | public void LogWarning(string format, params object[] args) 74 | { 75 | _logger.LogWarning(format, args); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/DatabaseTestBase.cs: -------------------------------------------------------------------------------- 1 | using XUnit.Hosting; 2 | 3 | namespace FluentCommand.PostgreSQL.Tests; 4 | 5 | [Collection(DatabaseCollection.CollectionName)] 6 | public abstract class DatabaseTestBase : TestHostBase 7 | { 8 | protected DatabaseTestBase(ITestOutputHelper output, DatabaseFixture databaseFixture) 9 | : base(output, databaseFixture) 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/FluentCommand.PostgreSQL.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | all 37 | runtime; build; native; contentfiles; analyzers 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Always 50 | 51 | 52 | Always 53 | 54 | 55 | Always 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/appsettings.appveyor.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=localhost;Port=5432;Database=TrackerTest;User Id=postgres;Password=Password12!;" 4 | } 5 | } -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/appsettings.github.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=localhost;Port=5432;Database=TrackerTest;User Id=postgres;Password=!P@ssw0rd;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/FluentCommand.PostgreSQL.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=ares;Port=5432;Database=TrackerTest;User Id=postgres;Password=!p@ssword;" 4 | } 5 | } -------------------------------------------------------------------------------- /test/FluentCommand.SQLite.Tests/DatabaseCollection.cs: -------------------------------------------------------------------------------- 1 | [assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] 2 | 3 | namespace FluentCommand.SQLite.Tests; 4 | 5 | [CollectionDefinition(DatabaseCollection.CollectionName)] 6 | public class DatabaseCollection : ICollectionFixture 7 | { 8 | public const string CollectionName = "DatabaseCollection"; 9 | } 10 | -------------------------------------------------------------------------------- /test/FluentCommand.SQLite.Tests/DatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.Sqlite; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | using XUnit.Hosting; 6 | 7 | namespace FluentCommand.SQLite.Tests; 8 | 9 | public class DatabaseFixture : TestApplicationFixture 10 | { 11 | protected override void ConfigureApplication(HostApplicationBuilder builder) 12 | { 13 | base.ConfigureApplication(builder); 14 | 15 | var services = builder.Services; 16 | 17 | services.AddHostedService(); 18 | 19 | services.AddFluentCommand(builder => builder 20 | .UseConnectionName("Tracker") 21 | .AddProviderFactory(SqliteFactory.Instance) 22 | .AddSqliteGenerator() 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/FluentCommand.SQLite.Tests/DatabaseInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using DbUp; 4 | using DbUp.Engine.Output; 5 | 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace FluentCommand.SQLite.Tests; 11 | 12 | public class DatabaseInitializer : IHostedService, IUpgradeLog 13 | { 14 | private readonly ILogger _logger; 15 | private readonly IConfiguration _configuration; 16 | 17 | public DatabaseInitializer(ILogger logger, IConfiguration configuration) 18 | { 19 | _logger = logger; 20 | _configuration = configuration; 21 | } 22 | 23 | 24 | public Task StartAsync(CancellationToken cancellationToken) 25 | { 26 | var connectionString = _configuration.GetConnectionString("Tracker"); 27 | 28 | var upgradeEngine = DeployChanges.To 29 | .SqliteDatabase(connectionString) 30 | .WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly()) 31 | .LogTo(this) 32 | .Build(); 33 | 34 | var result = upgradeEngine.PerformUpgrade(); 35 | 36 | return result.Successful 37 | ? Task.CompletedTask 38 | : Task.FromException(result.Error); 39 | } 40 | 41 | public Task StopAsync(CancellationToken cancellationToken) 42 | { 43 | return Task.CompletedTask; 44 | } 45 | 46 | 47 | public void LogDebug(string format, params object[] args) 48 | { 49 | _logger.LogDebug(format, args); 50 | } 51 | 52 | public void LogError(string format, params object[] args) 53 | { 54 | _logger.LogError(format, args); 55 | } 56 | 57 | public void LogError(Exception ex, string format, params object[] args) 58 | { 59 | _logger.LogError(ex, format, args); 60 | } 61 | 62 | public void LogInformation(string format, params object[] args) 63 | { 64 | _logger.LogInformation(format, args); 65 | } 66 | 67 | public void LogTrace(string format, params object[] args) 68 | { 69 | _logger.LogTrace(format, args); 70 | } 71 | 72 | public void LogWarning(string format, params object[] args) 73 | { 74 | _logger.LogWarning(format, args); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/FluentCommand.SQLite.Tests/DatabaseTestBase.cs: -------------------------------------------------------------------------------- 1 | using XUnit.Hosting; 2 | 3 | namespace FluentCommand.SQLite.Tests; 4 | 5 | [Collection(DatabaseCollection.CollectionName)] 6 | public abstract class DatabaseTestBase : TestHostBase 7 | { 8 | protected DatabaseTestBase(ITestOutputHelper output, DatabaseFixture databaseFixture) 9 | : base(output, databaseFixture) 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/FluentCommand.SQLite.Tests/FluentCommand.SQLite.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | all 36 | runtime; build; native; contentfiles; analyzers 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | Always 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /test/FluentCommand.SQLite.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Data Source=Tracker.db", 4 | "DistributedCache": "localhost:6379" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/DataCacheTests.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Entities; 2 | using FluentCommand.Query; 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace FluentCommand.SqlServer.Tests; 7 | 8 | public class DataCacheTests : DatabaseTestBase 9 | { 10 | public DataCacheTests(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture) 11 | { 12 | } 13 | 14 | [Fact] 15 | public async System.Threading.Tasks.Task SqlQueryInEntityAsync() 16 | { 17 | await using var session = Services.GetRequiredService(); 18 | session.Should().NotBeNull(); 19 | 20 | var values = new[] { 1, 2, 3 }; 21 | 22 | var results = await session 23 | .Sql(builder => builder 24 | .Select() 25 | .WhereIn(p => p.Id, values) 26 | ) 27 | .UseCache(TimeSpan.FromSeconds(5)) 28 | .QueryAsync(); 29 | 30 | results.Should().NotBeNull(); 31 | 32 | var list = results.ToList(); 33 | list.Count.Should().Be(3); 34 | 35 | var cacheResults = await session 36 | .Sql(builder => builder 37 | .Select() 38 | .WhereIn(p => p.Id, values) 39 | ) 40 | .UseCache(TimeSpan.FromSeconds(5)) 41 | .QueryAsync(); 42 | 43 | // check logs for cache hit 44 | var logs = GetLogEntries(); 45 | var hasHit = logs.Any(l => l.Message.Contains("Cache Hit;")); 46 | 47 | hasHit.Should().BeTrue(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/DataConfigurationTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | 3 | using FluentCommand.Caching; 4 | using FluentCommand.Query.Generators; 5 | 6 | using Microsoft.Data.SqlClient; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace FluentCommand.SqlServer.Tests; 10 | 11 | public class DataConfigurationTests : DatabaseTestBase 12 | { 13 | public DataConfigurationTests(ITestOutputHelper output, DatabaseFixture databaseFixture) 14 | : base(output, databaseFixture) 15 | { 16 | } 17 | 18 | [Fact] 19 | public void GetServices() 20 | { 21 | var services = Services; 22 | 23 | var factory = services.GetService(); 24 | factory.Should().NotBeNull(); 25 | 26 | var sqlFactory = services.GetService(); 27 | sqlFactory.Should().NotBeNull(); 28 | 29 | 30 | var generator = services.GetService(); 31 | generator.Should().NotBeNull(); 32 | 33 | var sqlGenerator = services.GetService(); 34 | sqlGenerator.Should().NotBeNull(); 35 | 36 | var dataCache = services.GetService(); 37 | dataCache.Should().NotBeNull(); 38 | 39 | var distributedCache = services.GetService(); 40 | distributedCache.Should().NotBeNull(); 41 | 42 | var sessionFactory = services.GetService(); 43 | sessionFactory.Should().NotBeNull(); 44 | 45 | var dataConfiguration = services.GetService(); 46 | dataConfiguration.Should().NotBeNull(); 47 | dataConfiguration.ConnectionString.Should().NotContain("Application Intent=ReadOnly"); 48 | 49 | var dataSession = services.GetService(); 50 | dataSession.Should().NotBeNull(); 51 | 52 | var readonlyFactory = services.GetService>(); 53 | readonlyFactory.Should().NotBeNull(); 54 | 55 | var readonlyConfiguration = services.GetService>(); 56 | readonlyConfiguration.Should().NotBeNull(); 57 | //readonlyConfiguration.ConnectionString.Should().Contain("Application Intent=ReadOnly"); 58 | 59 | var readonlySession = services.GetService>(); 60 | readonlySession.Should().NotBeNull(); 61 | 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/DataSessionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using Microsoft.Data.SqlClient; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace FluentCommand.SqlServer.Tests; 7 | 8 | public class DataSessionTests : DatabaseTestBase 9 | { 10 | public DataSessionTests(ITestOutputHelper output, DatabaseFixture databaseFixture) : base(output, databaseFixture) 11 | { 12 | } 13 | 14 | [Fact] 15 | public void CreateConnectionName() 16 | { 17 | var session = Services.GetRequiredService(); 18 | session.Should().NotBeNull(); 19 | session.Connection.Should().NotBeNull(); 20 | session.Connection.State.Should().Be(ConnectionState.Closed); 21 | } 22 | 23 | [Fact] 24 | public void CreateConnection() 25 | { 26 | var sqlConnection = new SqlConnection("Data Source=(local);Initial Catalog=Tracker;Integrated Security=True;"); 27 | var session = new DataSession(sqlConnection); 28 | session.Should().NotBeNull(); 29 | session.Connection.Should().NotBeNull(); 30 | session.Connection.State.Should().Be(ConnectionState.Closed); 31 | } 32 | 33 | [Fact] 34 | public void EnsureConnectionByName() 35 | { 36 | var session = Services.GetRequiredService(); 37 | session.Should().NotBeNull(); 38 | session.Connection.Should().NotBeNull(); 39 | session.Connection.State.Should().Be(ConnectionState.Closed); 40 | 41 | session.EnsureConnection(); 42 | session.Connection.State.Should().Be(ConnectionState.Open); 43 | 44 | session.ReleaseConnection(); 45 | session.Connection.State.Should().Be(ConnectionState.Closed); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/DatabaseCollection.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.SqlServer.Tests; 2 | 3 | [CollectionDefinition(DatabaseCollection.CollectionName)] 4 | public class DatabaseCollection : ICollectionFixture 5 | { 6 | public const string CollectionName = "DatabaseCollection"; 7 | } 8 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/DatabaseTestBase.cs: -------------------------------------------------------------------------------- 1 | using XUnit.Hosting; 2 | 3 | namespace FluentCommand.SqlServer.Tests; 4 | 5 | [Collection(DatabaseCollection.CollectionName)] 6 | public abstract class DatabaseTestBase : TestHostBase 7 | { 8 | protected DatabaseTestBase(ITestOutputHelper output, DatabaseFixture databaseFixture) 9 | : base(output, databaseFixture) 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/GeneratorTests.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Entities; 2 | using FluentCommand.Query; 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | using Task = System.Threading.Tasks.Task; 7 | 8 | namespace FluentCommand.SqlServer.Tests; 9 | 10 | public class GeneratorTests : DatabaseTestBase 11 | { 12 | public GeneratorTests(ITestOutputHelper output, DatabaseFixture databaseFixture) 13 | : base(output, databaseFixture) 14 | { 15 | } 16 | 17 | [Fact] 18 | public async Task QuerySelectStatusAsync() 19 | { 20 | var session = Services.GetRequiredService(); 21 | session.Should().NotBeNull(); 22 | 23 | var results = await session 24 | .Sql(builder => builder 25 | .Select() 26 | .OrderBy(p => p.DisplayOrder) 27 | .Limit(0, 1000) 28 | ) 29 | .QueryAsync(); 30 | 31 | results.Should().NotBeNull(); 32 | } 33 | 34 | [Fact] 35 | public async Task QuerySelectStatusRecordAsync() 36 | { 37 | var session = Services.GetRequiredService(); 38 | session.Should().NotBeNull(); 39 | 40 | var results = await session 41 | .Sql(builder => builder 42 | .Select() 43 | .OrderBy(p => p.DisplayOrder) 44 | .Limit(0, 1000) 45 | ) 46 | .QueryAsync(); 47 | 48 | results.Should().NotBeNull(); 49 | } 50 | 51 | [Fact] 52 | public async Task QuerySelectStatusReadOnlyAsync() 53 | { 54 | var session = Services.GetRequiredService(); 55 | session.Should().NotBeNull(); 56 | 57 | var results = await session 58 | .Sql(builder => builder 59 | .Select() 60 | .OrderBy(p => p.DisplayOrder) 61 | .Limit(0, 1000) 62 | ) 63 | .QueryAsync(); 64 | 65 | results.Should().NotBeNull(); 66 | } 67 | 68 | [Fact] 69 | public async Task QuerySelectStatusConstructorAsync() 70 | { 71 | var session = Services.GetRequiredService(); 72 | session.Should().NotBeNull(); 73 | 74 | var results = await session 75 | .Sql(builder => builder 76 | .Select() 77 | .OrderBy(p => p.DisplayOrder) 78 | .Limit(0, 1000) 79 | ) 80 | .QueryAsync(); 81 | 82 | results.Should().NotBeNull(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/ReadOnlyIntent.cs: -------------------------------------------------------------------------------- 1 | namespace FluentCommand.SqlServer.Tests; 2 | 3 | public readonly struct ReadOnlyIntent { } 4 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataMismatchTests.verified.txt: -------------------------------------------------------------------------------- 1 | MERGE INTO [dbo].[member_user] AS t 2 | USING 3 | ( 4 | VALUES 5 | ('00000000-0000-0000-0000-000000000000', 'test@email.com', 'Test User', 'Test', 'User'), 6 | ('00000000-0000-0000-0000-000000000000', 'blah@email.com', 'Blah User', 'Blah', 'User'), 7 | ('00000000-0000-0000-0000-000000000000', 'random@email.com', 'Random User', 'Random', 'User') 8 | ) 9 | AS s 10 | ( 11 | [Id], [email_address], [display_name], [first_name], [last_name] 12 | ) 13 | ON 14 | ( 15 | t.[email_address] = s.[email_address] 16 | ) 17 | WHEN NOT MATCHED BY TARGET THEN 18 | INSERT 19 | ( 20 | [Id], 21 | [email_address], 22 | [display_name], 23 | [first_name], 24 | [last_name] 25 | ) 26 | VALUES 27 | ( 28 | s.[Id], 29 | s.[email_address], 30 | s.[display_name], 31 | s.[first_name], 32 | s.[last_name] 33 | ) 34 | WHEN MATCHED THEN 35 | UPDATE SET 36 | t.[Id] = s.[Id], 37 | t.[display_name] = s.[display_name], 38 | t.[first_name] = s.[first_name], 39 | t.[last_name] = s.[last_name] 40 | ; -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataOutputTests.verified.txt: -------------------------------------------------------------------------------- 1 | MERGE INTO [dbo].[User] AS t 2 | USING 3 | ( 4 | VALUES 5 | ('test@email.com', 'Test User', 'Test', 'User', NULL, NULL), 6 | ('blah@email.com', 'Blah User', 'Blah', 'User', NULL, NULL), 7 | ('random@email.com', 'Random User', 'Random', 'User', NULL, NULL) 8 | ) 9 | AS s 10 | ( 11 | [EmailAddress], [DisplayName], [FirstName], [LastName], [LockoutEnd], [LastLogin] 12 | ) 13 | ON 14 | ( 15 | t.[EmailAddress] = s.[EmailAddress] 16 | ) 17 | WHEN NOT MATCHED BY TARGET THEN 18 | INSERT 19 | ( 20 | [EmailAddress], 21 | [DisplayName], 22 | [FirstName], 23 | [LastName], 24 | [LockoutEnd], 25 | [LastLogin] 26 | ) 27 | VALUES 28 | ( 29 | s.[EmailAddress], 30 | s.[DisplayName], 31 | s.[FirstName], 32 | s.[LastName], 33 | s.[LockoutEnd], 34 | s.[LastLogin] 35 | ) 36 | WHEN MATCHED THEN 37 | UPDATE SET 38 | t.[DisplayName] = s.[DisplayName], 39 | t.[FirstName] = s.[FirstName], 40 | t.[LastName] = s.[LastName], 41 | t.[LockoutEnd] = s.[LockoutEnd], 42 | t.[LastLogin] = s.[LastLogin] 43 | OUTPUT 44 | $action as [Action], 45 | DELETED.[EmailAddress] as [OriginalEmailAddress], 46 | INSERTED.[EmailAddress] as [CurrentEmailAddress], 47 | DELETED.[DisplayName] as [OriginalDisplayName], 48 | INSERTED.[DisplayName] as [CurrentDisplayName], 49 | DELETED.[FirstName] as [OriginalFirstName], 50 | INSERTED.[FirstName] as [CurrentFirstName], 51 | DELETED.[LastName] as [OriginalLastName], 52 | INSERTED.[LastName] as [CurrentLastName], 53 | DELETED.[LockoutEnd] as [OriginalLockoutEnd], 54 | INSERTED.[LockoutEnd] as [CurrentLockoutEnd], 55 | DELETED.[LastLogin] as [OriginalLastLogin], 56 | INSERTED.[LastLogin] as [CurrentLastLogin]; -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeDataTests.verified.txt: -------------------------------------------------------------------------------- 1 | MERGE INTO [dbo].[User] AS t 2 | USING 3 | ( 4 | VALUES 5 | ('test@email.com', 'Test User', 'Test', 'User', NULL, NULL), 6 | ('blah@email.com', 'Blah User', 'Blah', 'User', NULL, NULL) 7 | ) 8 | AS s 9 | ( 10 | [EmailAddress], [DisplayName], [FirstName], [LastName], [LockoutEnd], [LastLogin] 11 | ) 12 | ON 13 | ( 14 | t.[EmailAddress] = s.[EmailAddress] 15 | ) 16 | WHEN NOT MATCHED BY TARGET THEN 17 | INSERT 18 | ( 19 | [EmailAddress], 20 | [DisplayName], 21 | [FirstName], 22 | [LastName], 23 | [LockoutEnd], 24 | [LastLogin] 25 | ) 26 | VALUES 27 | ( 28 | s.[EmailAddress], 29 | s.[DisplayName], 30 | s.[FirstName], 31 | s.[LastName], 32 | s.[LockoutEnd], 33 | s.[LastLogin] 34 | ) 35 | WHEN MATCHED THEN 36 | UPDATE SET 37 | t.[DisplayName] = s.[DisplayName], 38 | t.[FirstName] = s.[FirstName], 39 | t.[LastName] = s.[LastName], 40 | t.[LockoutEnd] = s.[LockoutEnd], 41 | t.[LastLogin] = s.[LastLogin] 42 | ; -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildMergeTests.verified.txt: -------------------------------------------------------------------------------- 1 | MERGE INTO [dbo].[User] AS t 2 | USING 3 | ( 4 | SELECT 5 | [EmailAddress], 6 | [DisplayName], 7 | [FirstName], 8 | [LastName], 9 | [LockoutEnd], 10 | [LastLogin] 11 | FROM [#MergeTable] 12 | ) 13 | AS s 14 | ON 15 | ( 16 | t.[EmailAddress] = s.[EmailAddress] 17 | ) 18 | WHEN NOT MATCHED BY TARGET THEN 19 | INSERT 20 | ( 21 | [EmailAddress], 22 | [DisplayName], 23 | [FirstName], 24 | [LastName], 25 | [LockoutEnd], 26 | [LastLogin] 27 | ) 28 | VALUES 29 | ( 30 | s.[EmailAddress], 31 | s.[DisplayName], 32 | s.[FirstName], 33 | s.[LastName], 34 | s.[LockoutEnd], 35 | s.[LastLogin] 36 | ) 37 | WHEN MATCHED THEN 38 | UPDATE SET 39 | t.[DisplayName] = s.[DisplayName], 40 | t.[FirstName] = s.[FirstName], 41 | t.[LastName] = s.[LastName], 42 | t.[LockoutEnd] = s.[LockoutEnd], 43 | t.[LastLogin] = s.[LastLogin] 44 | ; -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/Snapshots/DataMergeGeneratorTests.BuildTableSqlTest.verified.txt: -------------------------------------------------------------------------------- 1 | CREATE TABLE [#MergeTable] 2 | ( 3 | [Id] bigint NULL, 4 | [Name] nvarchar(MAX) NULL, 5 | [Boolean] bit NULL, 6 | [Short] smallint NULL, 7 | [Long] bigint NULL, 8 | [Float] real NULL, 9 | [Double] float NULL, 10 | [Decimal] decimal NULL, 11 | [DateTime] datetime2 NULL, 12 | [DateTimeOffset] datetimeoffset NULL, 13 | [Guid] uniqueidentifier NULL, 14 | [TimeSpan] time NULL, 15 | [DateOnly] date NULL, 16 | [TimeOnly] time NULL, 17 | [BooleanNull] bit NULL, 18 | [ShortNull] smallint NULL, 19 | [LongNull] bigint NULL, 20 | [FloatNull] real NULL, 21 | [DoubleNull] float NULL, 22 | [DecimalNull] decimal NULL, 23 | [DateTimeNull] datetime2 NULL, 24 | [DateTimeOffsetNull] datetimeoffset NULL, 25 | [GuidNull] uniqueidentifier NULL, 26 | [TimeSpanNull] time NULL, 27 | [DateOnlyNull] date NULL, 28 | [TimeOnlyNull] time NULL 29 | ) 30 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/appsettings.appveyor.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=(local)\\SQL2019;Database=TrackerAppVeyor;Integrated security=SSPI;TrustServerCertificate=True;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/appsettings.github.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=localhost;Database=TrackerGithub;User Id=SA;Password=Bn87bBYhLjYRj%9zRgUc;TrustServerCertificate=True;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Data Source=(local);Initial Catalog=TrackerTest;Integrated Security=True;TrustServerCertificate=True;", 4 | "DistributedCache": "localhost:6379" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/FluentCommand.SqlServer.Tests/appsettings.linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Tracker": "Server=localhost,1401;Database=TrackerGithub;User Id=SA;Password=!P@ssw0rd;TrustServerCertificate=True;" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/FluentCommand.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers 27 | 28 | 29 | 30 | 31 | 32 | 33 | Analyzer 34 | false 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Internal/HashCodeTests.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | using FluentCommand.Tests.Models; 4 | 5 | using HashCode = FluentCommand.Internal.HashCode; 6 | 7 | namespace FluentCommand.Tests.Internal; 8 | 9 | public class HashCodeTests 10 | { 11 | [Fact] 12 | public void HashCommandConsistent() 13 | { 14 | var command = new Command 15 | { 16 | Text = "SELECT * FROM dbo.Status WHERE Type = @type OR Name = @name", 17 | CommandType = CommandType.Text, 18 | Parameters = 19 | { 20 | new Parameter{ Name = "@type", Value = 1, DbType = DbType.Int32 }, 21 | new Parameter{ Name = "@name", Value = "test", DbType = DbType.String }, 22 | } 23 | }; 24 | 25 | var commandHash = command.GetHashCode(); 26 | 27 | commandHash.Should().Be(92316680); 28 | } 29 | 30 | [Fact] 31 | public void HashStringConsistent() 32 | { 33 | var stringHash = HashCode.Seed 34 | .Combine("This is a test") 35 | .GetHashCode(); 36 | 37 | stringHash.Should().Be(1272545829); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Models/Command.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand.Tests.Models; 4 | 5 | public class Command 6 | { 7 | public string Text { get; set; } 8 | 9 | public CommandType CommandType { get; set; } 10 | 11 | public List Parameters { get; set; } = new List(); 12 | 13 | public override int GetHashCode() 14 | { 15 | return FluentCommand.Internal.HashCode.Seed 16 | .Combine(Text) 17 | .Combine(CommandType) 18 | .CombineAll(Parameters) 19 | .GetHashCode(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Models/Parameter.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | 3 | namespace FluentCommand.Tests.Models; 4 | 5 | public class Parameter 6 | { 7 | public string Name { get; set; } 8 | public object Value { get; set; } 9 | public DbType DbType { get; set; } 10 | 11 | public override int GetHashCode() 12 | { 13 | return FluentCommand.Internal.HashCode.Seed 14 | .Combine(Name) 15 | .Combine(Value) 16 | .Combine(DbType); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Models/Truck.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace FluentCommand.Tests.Models; 4 | 5 | [Table(nameof(Truck))] 6 | public class Truck : Vehicle 7 | { 8 | public string Color { get; set; } 9 | public override int Type { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | using FluentCommand.Handlers; 5 | 6 | namespace FluentCommand.Tests.Models; 7 | 8 | [Table(nameof(User))] 9 | public class User 10 | { 11 | public Guid Id { get; set; } 12 | 13 | public string EmailAddress { get; set; } 14 | 15 | public string DisplayName { get; set; } 16 | 17 | public string FirstName { get; set; } 18 | 19 | public string LastName { get; set; } 20 | 21 | public string PasswordHash { get; set; } 22 | 23 | public DateTimeOffset Created { get; set; } 24 | public string CreatedBy { get; set; } 25 | public DateTimeOffset Updated { get; set; } 26 | public string UpdatedBy { get; set; } 27 | 28 | [ConcurrencyCheck] 29 | [DatabaseGenerated(DatabaseGeneratedOption.Computed)] 30 | [DataFieldConverter(typeof(ConcurrencyTokenHandler))] 31 | public ConcurrencyToken RowVersion { get; set; } 32 | } 33 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Models/Vehicle.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace FluentCommand.Tests.Models; 4 | 5 | [Table(nameof(Vehicle))] 6 | public class Vehicle 7 | { 8 | public string Name { get; set; } 9 | public virtual int Type { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/DeleteBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Entities; 2 | using FluentCommand.Query; 3 | using FluentCommand.Query.Generators; 4 | 5 | namespace FluentCommand.Tests.Query; 6 | 7 | public class DeleteBuilderTest 8 | { 9 | [Fact] 10 | public async System.Threading.Tasks.Task DeleteEntityWithOutput() 11 | { 12 | var sqlProvider = new SqlServerGenerator(); 13 | var parameters = new List(); 14 | 15 | var builder = new DeleteEntityBuilder(sqlProvider, parameters) 16 | .Output(p => p.Id) 17 | .Where(p => p.Id, 1); 18 | 19 | var queryStatement = builder.BuildStatement(); 20 | 21 | var sql = queryStatement.Statement; 22 | 23 | await Verifier.Verify(sql).UseDirectory("Snapshots"); 24 | } 25 | 26 | [Fact] 27 | public async System.Threading.Tasks.Task DeleteEntityWithComment() 28 | { 29 | var sqlProvider = new SqlServerGenerator(); 30 | var parameters = new List(); 31 | 32 | var builder = new DeleteEntityBuilder(sqlProvider, parameters) 33 | .Tag() 34 | .Output(p => p.Id) 35 | .Where(p => p.Id, 1); 36 | 37 | var queryStatement = builder.BuildStatement(); 38 | 39 | var sql = queryStatement.Statement; 40 | 41 | await Verifier 42 | .Verify(sql) 43 | .UseDirectory("Snapshots") 44 | .ScrubLinesContaining("/* Caller;"); 45 | } 46 | 47 | [Fact] 48 | public async System.Threading.Tasks.Task DeleteEntityJoin() 49 | { 50 | var sqlProvider = new SqlServerGenerator(); 51 | var parameters = new List(); 52 | 53 | var builder = new DeleteEntityBuilder(sqlProvider, parameters) 54 | .Tag() 55 | .Output(p => p.Id) 56 | .From(tableAlias: "t") 57 | .Join(p => p 58 | .Left(p => p.PriorityId, "t") 59 | .Right(p => p.Id, "p") 60 | ) 61 | .Where(p => p.Id, 4, "p", FilterOperators.GreaterThanOrEqual); 62 | 63 | var queryStatement = builder.BuildStatement(); 64 | 65 | var sql = queryStatement.Statement; 66 | 67 | await Verifier 68 | .Verify(sql) 69 | .UseDirectory("Snapshots") 70 | .ScrubLinesContaining("/* Caller;"); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/InsertBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Entities; 2 | using FluentCommand.Query; 3 | using FluentCommand.Query.Generators; 4 | 5 | namespace FluentCommand.Tests.Query; 6 | 7 | public class InsertBuilderTest 8 | { 9 | [Fact] 10 | public async System.Threading.Tasks.Task InsertEntityValueWithOutput() 11 | { 12 | var sqlProvider = new SqlServerGenerator(); 13 | var parameters = new List(); 14 | 15 | var builder = new InsertEntityBuilder(sqlProvider, parameters) 16 | .Value(p => p.Name, "test") 17 | .Value(p => p.Description, "test") 18 | .Value(p => p.DisplayOrder, 10) 19 | .Value(p => p.Created, System.DateTimeOffset.UtcNow) 20 | .Value(p => p.Updated, System.DateTimeOffset.UtcNow) 21 | .Output(p => p.Id); 22 | 23 | var queryStatement = builder.BuildStatement(); 24 | 25 | var sql = queryStatement.Statement; 26 | 27 | await Verifier 28 | .Verify(sql) 29 | .UseDirectory("Snapshots") 30 | .ScrubLinesContaining("/* Caller;"); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/QueryBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Entities; 2 | using FluentCommand.Query; 3 | using FluentCommand.Query.Generators; 4 | 5 | namespace FluentCommand.Tests.Query; 6 | 7 | public class QueryBuilderTests 8 | { 9 | [Fact] 10 | public async System.Threading.Tasks.Task QueryBuilderSelect() 11 | { 12 | var sqlProvider = new SqlServerGenerator(); 13 | var queryParameters = new List(); 14 | var queryBuilder = new QueryBuilder(sqlProvider, queryParameters); 15 | 16 | queryBuilder.Select() 17 | .Column("Id") 18 | .Column(p => p.Name) 19 | .Column("Description") 20 | .Where(p => p.IsActive, true) 21 | .WhereOr(b => b 22 | .WhereOr(o => o 23 | .Where("Name", "Test", FilterOperators.Contains) 24 | .Where(p => p.Description, "Test", FilterOperators.Contains) 25 | ) 26 | ) 27 | .OrderBy(p => p.DisplayOrder, SortDirections.Descending) 28 | .OrderBy("Name"); 29 | 30 | var queryStatement = queryBuilder.BuildStatement(); 31 | 32 | var sql = queryStatement.Statement; 33 | 34 | await Verifier 35 | .Verify(sql) 36 | .UseDirectory("Snapshots") 37 | .ScrubLinesContaining("/* Caller;"); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/DeleteBuilderTest.DeleteEntityJoin.verified.txt: -------------------------------------------------------------------------------- 1 | DELETE FROM [Task] 2 | OUTPUT [DELETED].[Id] 3 | FROM [Task] AS [t] 4 | INNER JOIN [Priority] AS [p] ON [t].[PriorityId] = [p].[Id] 5 | WHERE ([p].[Id] >= @p0000); 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/DeleteBuilderTest.DeleteEntityWithComment.verified.txt: -------------------------------------------------------------------------------- 1 | DELETE FROM [dbo].[Status] 2 | OUTPUT [DELETED].[Id] 3 | WHERE ([Id] = @p0000); 4 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/DeleteBuilderTest.DeleteEntityWithOutput.verified.txt: -------------------------------------------------------------------------------- 1 | DELETE FROM [dbo].[Status] 2 | OUTPUT [DELETED].[Id] 3 | WHERE ([Id] = @p0000); 4 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/InsertBuilderTest.InsertEntityValueWithOutput.verified.txt: -------------------------------------------------------------------------------- 1 | INSERT INTO [dbo].[Status] ([Name], [Description], [DisplayOrder], [Created], [Updated]) 2 | OUTPUT [INSERTED].[Id] 3 | VALUES (@p0000, @p0001, @p0002, @p0003, @p0004); -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/QueryBuilderTests.QueryBuilderSelect.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name], [Description] 2 | FROM [dbo].[Status] 3 | WHERE ([IsActive] = @p0000 AND (([Name] LIKE '%' + @p0001 + '%' OR [Description] LIKE '%' + @p0002 + '%'))) 4 | ORDER BY [DisplayOrder] DESC, [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectColumnsAliasWhereTagBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [EntityId] AS [Id], [Name] 2 | FROM [EntityAlias] 3 | WHERE ([EntityId] = @p0000) 4 | ORDER BY [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityAliasWhereTagBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [EntityId] AS [Id], [Name] 2 | FROM [EntityAlias] 3 | WHERE ([EntityId] = @p0000) 4 | ORDER BY [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityChangeTableBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [t].[Id], [t].[EmailAddress], [t].[IsEmailAddressConfirmed], [t].[DisplayName], [t].[FirstName], [t].[LastName], [t].[PasswordHash], [t].[ResetHash], [t].[InviteHash], [t].[AccessFailedCount], [t].[LockoutEnabled], [t].[LockoutEnd], [t].[LastLogin], [t].[IsDeleted], [t].[Created], [t].[CreatedBy], [t].[Updated], [t].[UpdatedBy], [t].[RowVersion] 2 | FROM CHANGETABLE (CHANGES [User], @p0000) AS [c] 3 | INNER JOIN [User] AS [t] ON [c].[Id] = [t].[Id]; 4 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityFilterBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [t].[Id], [t].[EmailAddress], [t].[IsEmailAddressConfirmed], [t].[DisplayName], [t].[FirstName], [t].[LastName], [t].[ResetHash], [t].[InviteHash], [t].[AccessFailedCount], [t].[LockoutEnabled], [t].[LockoutEnd], [t].[LastLogin], [t].[IsDeleted], [t].[Created], [t].[CreatedBy], [t].[Updated], [t].[UpdatedBy], [t].[RowVersion] 2 | FROM [User]; 3 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityJoinBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [t].[Id], [t].[Description], [t].[DueDate], [u].[DisplayName], [u].[EmailAddress] AS [Email], [s].[Name] AS [Status] 2 | FROM [Task] AS [t] 3 | INNER JOIN [dbo].[Status] AS [s] ON [t].[StatusId] = [s].[Id] 4 | LEFT OUTER JOIN [User] AS [u] ON [t].[AssignedId] = [u].[Id] 5 | WHERE ([t].[PriorityId] = @p0000 AND [u].[EmailAddress] != @p0001) 6 | ORDER BY [t].[PriorityId] ASC; 7 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityNestedWhereBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name], [Description] 2 | FROM [dbo].[Status] 3 | WHERE ([IsActive] = @p0000 AND ([Name] LIKE '%' + @p0001 + '%' OR [Description] LIKE '%' + @p0002 + '%' OR ([IsActive] = @p0003 AND [DisplayOrder] > @p0004))) 4 | ORDER BY [DisplayOrder] DESC, [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityTemporalBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name] 2 | FROM [dbo].[Status] FOR SYSTEM_TIME AS OF @p0000 3 | WHERE ([Id] = @p0001) 4 | ORDER BY [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityWhereIn.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name] 2 | FROM [dbo].[Status] 3 | WHERE ([Id] IN (@p0000,@p0001)) 4 | ORDER BY [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityWhereInDouble.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name] 2 | FROM [dbo].[Status] 3 | WHERE ([Id] IN (@p0000,@p0001) AND [Name] IN (@p0002,@p0003)) 4 | ORDER BY [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityWhereInIf.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name] 2 | FROM [dbo].[Status] 3 | WHERE ([Id] IN (@p0000,@p0001)) 4 | ORDER BY [Name] ASC; 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityWhereLimitBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | SELECT [Id], [Name], [Description] 2 | FROM [dbo].[Status] 3 | WHERE ([IsActive] = @p0000) 4 | ORDER BY [DisplayOrder] DESC, [Name] ASC 5 | OFFSET 50 ROWS FETCH NEXT 25 ROWS ONLY; 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/SelectBuilderTest.SelectEntityWhereTagBuilder.verified.txt: -------------------------------------------------------------------------------- 1 | /* Select Entity Where Tag Builder Query; SelectEntityWhereTagBuilder() in SelectBuilderTest.cs:line 51 */ 2 | SELECT [Id], [Name], [Description] 3 | FROM [dbo].[Status] 4 | WHERE ([IsActive] = @p0000) 5 | ORDER BY [DisplayOrder] DESC, [Name] ASC; 6 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/UpdateBuilderTest.UpdateEntityValueWithJoin.verified.txt: -------------------------------------------------------------------------------- 1 | UPDATE [Task] 2 | SET [Description] = @p0000, [Updated] = @p0001 3 | OUTPUT [INSERTED].[Id] 4 | FROM [Task] AS [t] 5 | INNER JOIN [Priority] AS [p] ON [t].[PriorityId] = [p].[Id] 6 | WHERE ([p].[Id] >= @p0002); 7 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/Snapshots/UpdateBuilderTest.UpdateEntityValueWithOutput.verified.txt: -------------------------------------------------------------------------------- 1 | UPDATE [dbo].[Status] 2 | SET [Name] = @p0000, [Description] = @p0001, [DisplayOrder] = @p0002, [Created] = @p0003, [Updated] = @p0004 3 | OUTPUT [INSERTED].[Id] 4 | WHERE ([Id] = @p0005); 5 | -------------------------------------------------------------------------------- /test/FluentCommand.Tests/Query/UpdateBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using FluentCommand.Entities; 2 | using FluentCommand.Query; 3 | using FluentCommand.Query.Generators; 4 | 5 | namespace FluentCommand.Tests.Query; 6 | 7 | public class UpdateBuilderTest 8 | { 9 | [Fact] 10 | public async System.Threading.Tasks.Task UpdateEntityValueWithOutput() 11 | { 12 | var sqlProvider = new SqlServerGenerator(); 13 | var parameters = new List(); 14 | 15 | var builder = new UpdateEntityBuilder(sqlProvider, parameters) 16 | .Value(p => p.Name, "test") 17 | .Value(p => p.Description, "test") 18 | .Value(p => p.DisplayOrder, 10) 19 | .Value(p => p.Created, System.DateTimeOffset.UtcNow) 20 | .Value(p => p.Updated, System.DateTimeOffset.UtcNow) 21 | .Output(p => p.Id) 22 | .Where(p => p.Id, 1); 23 | 24 | var queryStatement = builder.BuildStatement(); 25 | 26 | var sql = queryStatement.Statement; 27 | 28 | await Verifier 29 | .Verify(sql) 30 | .UseDirectory("Snapshots") 31 | .ScrubLinesContaining("/* Caller;"); 32 | } 33 | 34 | [Fact] 35 | public async System.Threading.Tasks.Task UpdateEntityValueWithJoin() 36 | { 37 | var sqlProvider = new SqlServerGenerator(); 38 | var parameters = new List(); 39 | 40 | var builder = new UpdateEntityBuilder(sqlProvider, parameters) 41 | .Value(p => p.Description, "test") 42 | .Value(p => p.Updated, System.DateTimeOffset.UtcNow) 43 | .Output(p => p.Id) 44 | .From(tableAlias: "t") 45 | .Join(p => p 46 | .Left(p => p.PriorityId, "t") 47 | .Right(p => p.Id, "p") 48 | ) 49 | .Where(p => p.Id, 4, "p", FilterOperators.GreaterThanOrEqual); 50 | 51 | var queryStatement = builder.BuildStatement(); 52 | 53 | var sql = queryStatement.Statement; 54 | 55 | await Verifier 56 | .Verify(sql) 57 | .UseDirectory("Snapshots") 58 | .ScrubLinesContaining("/* Caller;"); 59 | } 60 | } 61 | --------------------------------------------------------------------------------