├── .github └── workflows │ └── dotnet-core.yml ├── .gitignore ├── FastExpressionKit.BulkInsert ├── BulkInserter.cs └── FastExpressionKit.BulkInsert.csproj ├── FastExpressionKit.Integration.Tests ├── Benchmarks.cs ├── FastExpressionKit.Integration.Tests.csproj ├── IntegrationTests.cs └── Program.cs ├── FastExpressionKit.Test ├── FastExpressionKit.Test.csproj ├── Program.cs ├── Test.cs ├── TestDataPocClass.cs ├── TestDataPocClass.tt ├── TestEntities.cs └── TrivialValidator.cs ├── FastExpressionKit.sln ├── FastExpressionKit ├── FastExpressionKit.cs └── FastExpressionKit.csproj ├── LICENSE ├── NuGet.Config ├── README.md ├── azure-pipelines.yml ├── keypair.snk ├── publish.py └── test.py /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup .NET Core 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 3.1.301 19 | - name: Test 20 | run: python3 test.py 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | *.vcxproj.filters 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # .NET Core 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | **/Properties/launchSettings.json 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 246 | *.vbw 247 | 248 | # Visual Studio LightSwitch build output 249 | **/*.HTMLClient/GeneratedArtifacts 250 | **/*.DesktopClient/GeneratedArtifacts 251 | **/*.DesktopClient/ModelManifest.xml 252 | **/*.Server/GeneratedArtifacts 253 | **/*.Server/ModelManifest.xml 254 | _Pvt_Extensions 255 | 256 | # Paket dependency manager 257 | .paket/paket.exe 258 | paket-files/ 259 | 260 | # FAKE - F# Make 261 | .fake/ 262 | 263 | # JetBrains Rider 264 | .idea/ 265 | *.sln.iml 266 | 267 | # CodeRush 268 | .cr/ 269 | 270 | # Python Tools for Visual Studio (PTVS) 271 | __pycache__/ 272 | *.pyc 273 | 274 | # Cake - Uncomment if you are using it 275 | # tools/ 276 | 277 | /.ionide/ 278 | -------------------------------------------------------------------------------- /FastExpressionKit.BulkInsert/BulkInserter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text; 8 | 9 | namespace FastExpressionKit.BulkInsert 10 | { 11 | 12 | public class TableMappingRules 13 | { 14 | public string TableName { get; set; } 15 | public Func ColumnNameGenerator { get; set; } 16 | 17 | public static string ToUnderscoreCase(string str) { 18 | // xx rewrite without linq 19 | return string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? 20 | "_" + x.ToString() : x.ToString())).ToLower(); 21 | } 22 | 23 | public static string GuessColumnNameBasedOnPropertyInfo(PropertyInfo pi) 24 | { 25 | return ToUnderscoreCase(pi.Name).ToUpperInvariant(); 26 | } 27 | 28 | public static TableMappingRules UpperSnake(string tableName) => 29 | new TableMappingRules 30 | { 31 | TableName = tableName, 32 | ColumnNameGenerator = GuessColumnNameBasedOnPropertyInfo 33 | }; 34 | } 35 | 36 | // these are compatible with a proprietary database driver 37 | public enum DbParameterTypeNumbers 38 | { 39 | Unknown = 0, 40 | Date = 8, 41 | Double = 9, 42 | Float = 10, 43 | Integer = 11, 44 | NVarChar = 18, 45 | Number = 19, 46 | Raw = 22, 47 | TimeStamp = 25, 48 | VarChar = 28, 49 | } 50 | 51 | [DebuggerDisplay("{Name}: {FieldType}")] 52 | public class MapperPropertyData 53 | { 54 | public string Name; 55 | public string DbColumnName; 56 | public string ParameterNameInQuery; 57 | public Type FieldType { get; set; } 58 | public DbParameterTypeNumbers RecommendedDbType { get; set; } 59 | } 60 | 61 | // you can use this to construct DbParameter objects for your db driver 62 | // e.g. OracleParameter 63 | [DebuggerDisplay("{ParameterName}: {DbParamType} count={Values.Length}")] 64 | public class BulkInsertInstructions 65 | { 66 | public object[] Values { get; set; } 67 | 68 | public DbParameterTypeNumbers DbParamType { get; set; } 69 | public string ParameterName { get; set; } 70 | 71 | } 72 | 73 | 74 | public class FastBulkInserter 75 | { 76 | 77 | public string TableName { get; set; } 78 | public IReadOnlyList Properties { get; set; } 79 | public FieldExtract FieldExtractor { get; set; } 80 | public string InsertSql { get; set; } 81 | public IReadOnlyList BuildInstructionsForRows(IReadOnlyList rows) 82 | { 83 | var objs = FieldExtractUtil.ExtractToObjectArrays(FieldExtractor, rows); 84 | var instructions = new BulkInsertInstructions[Properties.Count]; 85 | 86 | for (var i=0; i < Properties.Count; i++) 87 | { 88 | var prop = Properties[i]; 89 | instructions[i] = new BulkInsertInstructions 90 | { 91 | DbParamType = prop.RecommendedDbType, 92 | ParameterName = prop.ParameterNameInQuery, 93 | Values = objs[i] 94 | }; 95 | } 96 | return instructions; 97 | } 98 | } 99 | 100 | // use as singleton 101 | public static class FastBulkInsertCache 102 | { 103 | // no point in concurrent dictionary because it has to be populated well ahead of time 104 | 105 | private static Dictionary cache = new Dictionary(); 106 | 107 | public static void Add(FastBulkInserter inserter) 108 | { 109 | cache.Add(typeof(TEntity), inserter); 110 | } 111 | 112 | public static FastBulkInserter Get() 113 | { 114 | return (FastBulkInserter) cache[typeof(TEntity)]; 115 | } 116 | } 117 | public static class FastBulkInsertUtil 118 | { 119 | public static readonly Dictionary ParameterTypeMap = new Dictionary 120 | { 121 | [typeof(Guid)] = DbParameterTypeNumbers.Raw, 122 | [typeof(DateTime)] = DbParameterTypeNumbers.Date, 123 | [typeof(DateTime?)] = DbParameterTypeNumbers.Date, 124 | [typeof(string)] = DbParameterTypeNumbers.NVarChar, 125 | [typeof(Int32)] = DbParameterTypeNumbers.Number, 126 | [typeof(Int32?)] = DbParameterTypeNumbers.Number, 127 | [typeof(Int16)] = DbParameterTypeNumbers.Number, 128 | [typeof(Int16?)] = DbParameterTypeNumbers.Number, 129 | [typeof(decimal)] = DbParameterTypeNumbers.Number, 130 | [typeof(decimal?)] = DbParameterTypeNumbers.Number, 131 | }; 132 | 133 | 134 | 135 | // you should use this to create inserters BUT you should cache them 136 | // if rules are used, column names are guessed 137 | // yeah use [Table] and [Column] instead 138 | public static FastBulkInserter CreateBulkInserter(TableMappingRules rules = null) 139 | { 140 | var guessingMode = rules != null; 141 | var props = ReflectionHelper.GetProps(); 142 | 143 | var tableAttrs = typeof(TEntity).GetCustomAttributes(typeof(TableAttribute), true); 144 | string tableName = null; 145 | 146 | if (rules != null) 147 | { 148 | tableName = rules.TableName; 149 | } 150 | else 151 | { 152 | tableName = tableAttrs.Length > 0 ? ((TableAttribute)tableAttrs[0]).Name : null; 153 | } 154 | var mappedProps = new List(); 155 | var sql = new StringBuilder(); 156 | sql.Append("INSERT INTO "); 157 | sql.Append(tableName); 158 | sql.Append(" ("); 159 | 160 | // yeah we build this at the same cycle 161 | 162 | var valuesSql = new StringBuilder(); 163 | valuesSql.Append("VALUES ("); 164 | int index = 0; 165 | var mappedColumnNames = new List(props.Length); 166 | foreach (var prop in props) 167 | { 168 | var columnAttr = prop.GetCustomAttributes(typeof(ColumnAttribute), true ); 169 | // we only take first column attribute 170 | var columnName = columnAttr.Length > 0 ? ((ColumnAttribute) columnAttr[0]).Name : null; 171 | 172 | if (columnName == null && !guessingMode) 173 | { 174 | // we skip columns with no [Column] attribute 175 | continue; 176 | } 177 | 178 | var typeOk = ParameterTypeMap.TryGetValue(prop.PropertyType, out var recommendedParamType); 179 | if (!typeOk && prop.PropertyType.IsEnum) 180 | { 181 | typeOk = true; 182 | recommendedParamType = DbParameterTypeNumbers.Number; 183 | } 184 | 185 | if (!typeOk) 186 | { 187 | // skip unknown parameters. TODO: log this? 188 | // example of skipped types: collections, navigation props etc 189 | continue; 190 | } 191 | if (guessingMode) 192 | { 193 | columnName = rules.ColumnNameGenerator(prop); 194 | // a way to skip property - return null from name generator 195 | if (columnName == null) 196 | { 197 | continue; 198 | } 199 | } 200 | 201 | var paramNameInQuery = ":B" + index.ToString(); 202 | mappedProps.Add(new MapperPropertyData { 203 | Name = prop.Name, 204 | DbColumnName = columnName, 205 | FieldType = prop.PropertyType, 206 | ParameterNameInQuery = paramNameInQuery, 207 | RecommendedDbType = recommendedParamType 208 | 209 | }); 210 | mappedColumnNames.Add(prop.Name); 211 | sql.Append(columnName); 212 | sql.Append(","); 213 | index++; 214 | valuesSql.Append(paramNameInQuery); 215 | valuesSql.Append(","); 216 | }; 217 | 218 | // remove last ',' and close both strings 219 | sql.Length--; 220 | sql.Append(") "); 221 | valuesSql.Length--; 222 | valuesSql.Append(')'); 223 | 224 | sql.Append(valuesSql); 225 | var finalSql = sql.ToString(); 226 | var extractor = new FieldExtract(mappedColumnNames.ToArray()); 227 | var bulkInserter = new FastBulkInserter() 228 | { 229 | InsertSql = finalSql, 230 | Properties = mappedProps, 231 | TableName = tableName, 232 | FieldExtractor = extractor 233 | }; 234 | return bulkInserter; 235 | } 236 | 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /FastExpressionKit.BulkInsert/FastExpressionKit.BulkInsert.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FastExpressionKit.BulkInsert 5 | netstandard2.0 6 | 7 | Library to quick bulk insert entities to database 8 | vivainio 9 | 10 | Ville Vainio 11 | https://github.com/vivainio/FastExpressionKit 12 | https://github.com/vivainio/FastExpressionKit/blob/master/LICENSE 13 | 14 | 15 | true 16 | ../keypair.snk 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /FastExpressionKit.Integration.Tests/Benchmarks.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using BenchmarkDotNet.Attributes; 3 | 4 | namespace FastExpressionKit.Integration.Tests 5 | { 6 | public class Benchmarks 7 | { 8 | [Benchmark] 9 | public void Hello() 10 | { 11 | Thread.Sleep(100); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /FastExpressionKit.Integration.Tests/FastExpressionKit.Integration.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net472 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ..\..\..\o\3rdParty\Devart\Devart.Data.dll 22 | 23 | 24 | ..\..\..\o\3rdParty\Devart\Devart.Data.Oracle.dll 25 | 26 | 27 | ..\..\..\o\3rdParty\Devart\Devart.Data.Oracle.Entity.EF6.dll 28 | 29 | 30 | ..\..\..\o\3rdParty\Devart\Devart.Data.Oracle.Entity.Spatials.EF6.dll 31 | 32 | 33 | ..\..\..\o\3rdParty\Devart\Devart.Data.Oracle.Vs.dll 34 | 35 | 36 | ..\..\..\o\3rdParty\Devart\Devart.Data.Oracle.Web.dll 37 | 38 | 39 | ..\..\..\o\3rdParty\Devart\Devart.Data.Oracle.Web.Identity.dll 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /FastExpressionKit.Integration.Tests/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using Devart.Data.Oracle; 3 | using FastExpressionKit.BulkInsert; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Data.Common; 8 | using System.Data.Entity; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | using TrivialTestRunner; 14 | 15 | namespace FastExpressionKit.Integration.Tests 16 | { 17 | 18 | public static class DevartFastBulkInsert 19 | { 20 | public static void FastBulkInsert(IReadOnlyList rows, IDbTransaction transaction) 21 | { 22 | 23 | // warning, oracle specific code 24 | var bi = FastBulkInsertUtil.CreateBulkInserter(); 25 | var instr = bi.BuildInstructionsForRows(rows); 26 | var tx = (OracleTransaction)transaction; 27 | var conn = (OracleConnection) transaction.Connection; 28 | var inputLen = rows.Count; 29 | var command = conn.CreateCommand(bi.InsertSql, CommandType.Text); 30 | var allParams = instr.Select(it => 31 | { 32 | 33 | var param = new OracleParameter(it.ParameterName, 34 | (OracleDbType)(int)it.DbParamType, 35 | it.Values, 36 | System.Data.ParameterDirection.Input); 37 | param.ArrayLength = inputLen; 38 | return param; 39 | }).ToArray(); 40 | command.Parameters.AddRange(allParams); 41 | command.ExecuteArray(inputLen); 42 | } 43 | 44 | public static void FastBulkInsertWithConnection(IReadOnlyList rows, DbConnection conn) 45 | { 46 | var needClosing = false; 47 | if (conn.State == System.Data.ConnectionState.Closed) 48 | { 49 | conn.Open(); 50 | needClosing = true; 51 | }; 52 | 53 | using (var transaction = conn.BeginTransaction()) 54 | { 55 | try 56 | { 57 | FastBulkInsert(rows, transaction); 58 | transaction.Commit(); 59 | } 60 | catch (Exception) 61 | { 62 | transaction.Rollback(); 63 | if (needClosing) 64 | { 65 | conn.Close(); 66 | } 67 | throw; 68 | } 69 | } 70 | } 71 | 72 | 73 | public static void FastBulkInsertWithEfContext(this DbContext context, IReadOnlyList rows) 74 | { 75 | var conn = context.Database.Connection; 76 | FastBulkInsertWithConnection(rows, conn); 77 | } 78 | } 79 | 80 | 81 | public class IntegrationTests 82 | 83 | { 84 | static string ConnectionStringForIntegrationTest => File.ReadAllText(@"C:\o\3rdParty\Devart\connection_string.txt").Trim(); 85 | 86 | static OracleConnection GetConnection() => new OracleConnection(ConnectionStringForIntegrationTest); 87 | 88 | [Case] 89 | public static void ConnectToLocalDbWithDevart() 90 | { 91 | 92 | var conn = new OracleConnection(ConnectionStringForIntegrationTest); 93 | conn.Open(); 94 | conn.Close(); 95 | 96 | } 97 | [Case] 98 | public static void InsertFew() 99 | { 100 | var f = new Fixture(); 101 | var testentities = f.CreateMany(1000).ToArray(); 102 | var conn = GetConnection(); 103 | DevartFastBulkInsert.FastBulkInsertWithConnection(testentities, conn); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /FastExpressionKit.Integration.Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Configs; 3 | using BenchmarkDotNet.Running; 4 | using TrivialTestRunner; 5 | 6 | namespace FastExpressionKit.Integration.Tests 7 | { 8 | class Program 9 | { 10 | static void RunBenchmarks() 11 | { 12 | var config = DefaultConfig.Instance.With(ConfigOptions.DisableOptimizationsValidator); 13 | var summary = BenchmarkRunner.Run(typeof(Program).Assembly, config); 14 | } 15 | 16 | static void Main(string[] args) 17 | 18 | { 19 | if (args.Length > 0) 20 | { 21 | RunBenchmarks(); 22 | 23 | } 24 | else 25 | { 26 | TRunner.CrashHard = false; 27 | TRunner.AddTests(); 28 | TRunner.RunTests(); 29 | TRunner.ReportAll(); 30 | 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/FastExpressionKit.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Exe 22 | netcoreapp3.1 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using TrivialTestRunner; 3 | using FastExpressionKitTests; 4 | using System.Threading.Tasks; 5 | 6 | namespace FastExpressionKit.Test 7 | { 8 | class Program 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | // use while debugging 13 | // TRunner.CrashHard = true; 14 | TRunner.AddTests(); 15 | await TRunner.RunTestsAsync(); 16 | TRunner.ReportAll(); 17 | return TRunner.ExitStatus; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/Test.cs: -------------------------------------------------------------------------------- 1 | using FakePoc; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using static System.Console; 8 | using TrivialTestRunner; 9 | using FastExpressionKit; 10 | using Newtonsoft.Json; 11 | using System.ComponentModel.DataAnnotations.Schema; 12 | using AutoFixture; 13 | using FastExpressionKit.BulkInsert; 14 | using FastExpressionKit.Integration.Tests; 15 | using FastExpressionKit.Test; 16 | using NFluent; 17 | 18 | namespace FastExpressionKitTests 19 | { 20 | public class C 21 | { 22 | public int a { get; set; } 23 | public int b { get; set; } 24 | public string s { get; set; } 25 | public DateTime date { get; set; } 26 | public DateTime? mynullable { get; set; } 27 | 28 | public int sometimesnullable { get; set; } 29 | } 30 | public class D 31 | { 32 | [Column("A_COL")] 33 | public int a { get; set; } 34 | [Column("B_COL")] 35 | 36 | public int b { get; set; } 37 | 38 | [Column("C_COL")] 39 | public int c { get; set; } 40 | 41 | [Column("DATE_COL")] 42 | public DateTime date { get; set; } 43 | 44 | public string NoTable { get; set; } 45 | [Column("DATE_NULLABLE_COL")] 46 | 47 | public DateTime? mynullable { get; set; } 48 | 49 | public int? sometimesnullable { get; set; } 50 | 51 | } 52 | 53 | public class SomeReadOnly 54 | { 55 | public int a { get; set; } 56 | public int b { get; set; } 57 | public int readOnly { get; } 58 | 59 | } 60 | 61 | public class SomeStrings 62 | { 63 | public string a { get; set; } 64 | public string b { get; set; } 65 | public string c 66 | { 67 | get; 68 | set; 69 | } 70 | public int surpriseinteger { get; set; } 71 | } 72 | 73 | public class SomeDecimals 74 | { 75 | public decimal a { get; set; } 76 | public decimal b { get; set; } 77 | public decimal? cnullable { get; set; } 78 | } 79 | 80 | class FastExprKitTest 81 | { 82 | static void RepeatBench(string description, int n, Action action) 83 | { 84 | var sw = Stopwatch.StartNew(); 85 | for (var i = 0; i < n; i++) 86 | { 87 | action(); 88 | } 89 | 90 | WriteLine("{0}: {1}", description, (sw.ElapsedMilliseconds * 1000) / (float)n); 91 | 92 | } 93 | 94 | public static void ValidateString(Type t, string key, string value, char[] badChars) 95 | { 96 | var loc = value.IndexOfAny(badChars); 97 | if (loc != -1) 98 | throw new ValidationError($"Type {t} Prop {key}, String contained illegal character '{value[loc]}' at position {loc}"); 99 | } 100 | 101 | public static void ValidateDecimal(Type t, string key, decimal value, bool extra) 102 | { 103 | 104 | if (value < 0) 105 | throw new ValidationError($"Type {t} Property {key} was negative: {value}"); 106 | } 107 | 108 | [Case] 109 | public static void TestTrivialValidator() 110 | { 111 | TrivialValidator.SetValidator(prop => 112 | prop.PropertyType == typeof(string) 113 | && prop.Name != "c", TrivialValidator.BadCharacters); 114 | var sut = new SomeStrings() 115 | { 116 | a = "my a", 117 | b = "O'Reilly", // this will raise error 118 | c = "the ceeeee" 119 | }; 120 | 121 | Check.ThatCode(() => 122 | { 123 | TrivialValidator.Validate(sut); 124 | }).Throws(); 125 | // 2 times should work as well 126 | Check.ThatCode(() => 127 | { 128 | TrivialValidator.Validate(sut); 129 | }).Throws(); 130 | 131 | Check.ThatCode(() => 132 | { 133 | TrivialValidator.ValidateMany(new[] { sut, sut }); 134 | }).Throws(); 135 | 136 | 137 | TrivialValidator.SetValidator( prop => prop.Name != "b", 138 | TrivialValidator.BadCharacters, 139 | overwrite: true); 140 | TrivialValidator.Validate(sut); // will not throw 141 | } 142 | [Case] 143 | public static void TestForEach() 144 | { 145 | var mi = typeof(FastExprKitTest).GetMethod("ValidateString"); 146 | var fe = new RunMethodForEachProperty(new[] { "a", "b", "c" }, mi, TrivialValidator.BadCharacters); 147 | var sut = new SomeStrings() 148 | { 149 | a = "my a", 150 | b = "O'Reilly", // this will raise error 151 | c = "the ceeeee" 152 | }; 153 | Check.ThatCode(() => 154 | { 155 | fe.Run(sut); 156 | }).Throws(); 157 | sut.b = "Nondangerous string"; 158 | fe.Run(sut); // does not throw 159 | 160 | var decimalChecker = new RunMethodForEachProperty(new[] { "a", "b" }, 161 | typeof(FastExprKitTest).GetMethod(nameof(ValidateDecimal)), true); 162 | 163 | var dd = new SomeDecimals 164 | { 165 | a = 12, 166 | b = -2, 167 | cnullable = 3 168 | }; 169 | Check.ThatCode(() => 170 | { 171 | decimalChecker.Run(dd); 172 | }).Throws(); 173 | 174 | } 175 | [Case] 176 | public static void Benchmark() 177 | { 178 | var c1 = new C() { a = 666, b = 12, date = DateTime.Now, mynullable = DateTime.Now }; 179 | var d1 = new D() { a = 666, b = 8, c = 9, date = DateTime.Now, mynullable = DateTime.Now }; 180 | 181 | var fields = new[] { "a", "b" }; 182 | 183 | WriteLine("Stats (microseconds) per iteration"); 184 | 185 | RepeatBench("new Differ()", 1000, () => 186 | { 187 | var dd = new Differ(fields); 188 | }); 189 | 190 | var d = new Differ(fields); 191 | 192 | RepeatBench("Compare small objects", 1000000, () => 193 | { 194 | var res = d.Compare(c1, d1); 195 | }); 196 | RepeatBench("new FieldExtract()", 1000, () => 197 | { 198 | var dd = new FieldExtract(fields); 199 | }); 200 | var extractor = new FieldExtract(fields); 201 | 202 | RepeatBench("Extract integers", 1000000, () => 203 | { 204 | var ints = extractor.Extract(c1); 205 | }); 206 | 207 | // big data 208 | var big1 = new BigDto(); 209 | var big2 = new BigDto(); 210 | 211 | var bigprops = ReflectionHelper.CollectProps(); 212 | var bigpropnames = bigprops.SelectMany(p => p.Item2).ToArray(); 213 | 214 | RepeatBench("new Differ() for big class", 100, () => 215 | { 216 | var dd = new Differ(bigpropnames); 217 | 218 | }); 219 | 220 | var bigd = new Differ(bigpropnames); 221 | RepeatBench("Compare large objects", 10000, () => { bigd.Compare(big1, big2); }); 222 | 223 | var types = ReflectionHelper.CollectProps(); 224 | var e4 = ReflectionHelper.GetExtractorFor(types); 225 | var e5 = ReflectionHelper.GetExtractorFor(types); 226 | var e6 = ReflectionHelper.GetExtractorFor(types); 227 | 228 | 229 | RepeatBench("Extract fields from large object", 10000, () => 230 | { 231 | var r1 = e4.Extract(big1); 232 | var r2 = e5.Extract(big1); 233 | var r3 = e6.Extract(big1); 234 | }); 235 | 236 | 237 | RepeatBench("Extract fields from large object, convert to dict, naive", 10000, () => 238 | { 239 | var pd = e4.ResultsAsDict(e4.Extract(big1).Select(i => i.ToString()).ToList()) 240 | .Union(e5.ResultsAsDict(e5.Extract(big1).Select(e => e.ToString()).ToList())) 241 | .Union(e6.ResultsAsDict(e6.Extract(big1).Select(e => e.ToString()).ToList())); 242 | }); 243 | 244 | var boxedExtract = new FieldExtract(bigpropnames); 245 | 246 | RepeatBench("Extract fields, boxed", 100000, () => 247 | { 248 | var r1 = boxedExtract.Extract(big1); 249 | }); 250 | 251 | RepeatBench("Extract fields from large dto. string -> object dict", 10000, () => 252 | { 253 | var r1 = boxedExtract.Extract(big1); 254 | var r2 = boxedExtract.ResultsAsZip(r1); 255 | }); 256 | 257 | 258 | var propertyInfos = typeof(BigDto).GetProperties(); 259 | RepeatBench("Extract fields with reflection", 10000, () => 260 | { 261 | foreach (var p in propertyInfos) 262 | { 263 | var val = p.GetValue(big1); 264 | } 265 | }); 266 | 267 | RepeatBench("Extract fields with reflection, convert to string dict", 10000, () => 268 | { 269 | var resdict = new Dictionary(); 270 | foreach (var p in propertyInfos) 271 | { 272 | var val = p.GetValue(big1); 273 | var s = val.ToString(); 274 | resdict[p.Name] = s; 275 | } 276 | }); 277 | 278 | 279 | var copier = new FieldCopier(bigpropnames); 280 | RepeatBench("Copy big object", 100000, () => { copier.Copy(big1, big2); }); 281 | 282 | } 283 | 284 | static DateTime SomeDate = new DateTime(2020, 2, 24); 285 | 286 | // test data for small objects 287 | static C c1 = new C() { a = 666, b = 12, date = DateTime.Now, mynullable = DateTime.Now, s = "one" }; 288 | static C c2 = new C() { a = 100, b = 12, mynullable = null, s = "two" }; 289 | static D d1 = new D() { a = 666, b = 12, c = 123, date = SomeDate, NoTable = "not mapped", mynullable = null }; 290 | static D d2 = new D() { a = 100, b = 12, c = 223, date = SomeDate, mynullable = SomeDate }; 291 | static string[] fields = new[] { "a", "b" }; 292 | 293 | [Case] 294 | public static void TestCopier() 295 | { 296 | c1.a = 111; 297 | c1.b = 999; 298 | var copier = new FieldCopier(fields); 299 | copier.Copy(c1, c2); 300 | } 301 | 302 | public static void ManyExtractors() 303 | { 304 | // automatically create extractors per each property type 305 | var types = ReflectionHelper.CollectProps(); 306 | var e4 = ReflectionHelper.GetExtractorFor(types); 307 | var e5 = ReflectionHelper.GetExtractorFor(types); 308 | var e6 = ReflectionHelper.GetExtractorFor(types); 309 | 310 | Action tryit = c => 311 | { 312 | // smash exctractor results together with conversions to get string 313 | 314 | var pd = e4.ResultsAsDict(e4.Extract(c).Select(i => i.ToString()).ToList()) 315 | .Union(e5.ResultsAsDict(e5.Extract(c).Select(e => e.ToString()).ToList())) 316 | .Union(e6.ResultsAsDict(e6.Extract(c).Select(e => e.ToString()).ToList())); 317 | }; 318 | 319 | tryit(c1); 320 | tryit(c2); 321 | } 322 | 323 | [Case] 324 | public static void TestFieldExtract() 325 | { 326 | var extractor = new FieldExtract(fields); 327 | var results = extractor.Extract(c1); 328 | //var dresults = extractor.ExtractAsDict(c1); 329 | try 330 | { 331 | var fails = new FieldExtract(fields); 332 | } 333 | catch (InvalidOperationException) 334 | { 335 | } 336 | 337 | var ee = new FieldExtract(new[] { "mynullable" }); 338 | var r = ee.Extract(c1); 339 | 340 | //var e2 = new FieldExtract(fields); 341 | //e2.Extract(c1); 342 | 343 | var e3 = new FieldExtract(new[] { "date" }); 344 | var r3 = e3.Extract(c1); 345 | } 346 | 347 | [Case] 348 | public static void TestExtractToArrays() 349 | { 350 | var extractor = new FieldExtract(new[] { "a", "b", "s" }); 351 | var results = extractor.Extract(c1); 352 | var extracted = FieldExtractUtil.ExtractToObjectArrays(extractor, new[] { c1, c2, c1, c2 }); 353 | var serialized = JsonConvert.SerializeObject(extracted); 354 | Assert.AreEqual(serialized, @"[[100,100,100,100],[12,12,12,12],[""one"",""two"",""one"",""two""]]"); 355 | } 356 | 357 | [Case] 358 | public static void DifferSmall() 359 | { 360 | var differ = new Differ(new[] { "a", "b" }); 361 | var res = differ.Compare(c1, c2); 362 | 363 | // compare different types! 364 | var differ2 = new Differ(new[] { "a", "b" }); 365 | res = differ2.Compare(c1, d1); 366 | } 367 | 368 | [Case] 369 | public static void TestReflectionHelper() 370 | { 371 | var writeable = ReflectionHelper.WriteablePropNames(); 372 | //Assert.Contains("a", writeable); 373 | Assert.IsTrue(!writeable.Contains("readOnly")); 374 | } 375 | 376 | [Case] 377 | public static void DbBulkInsert() 378 | { 379 | var inserter = FastBulkInsertUtil.CreateBulkInserter(); 380 | var dtos = new[] { d1, d2 }; 381 | var instructions = inserter.BuildInstructionsForRows(dtos); 382 | Check.That(instructions).CountIs(5); 383 | } 384 | 385 | [Case] 386 | public static void TestCachedInserters() 387 | { 388 | var i1 = FastBulkInsertUtil.CreateBulkInserter(); 389 | FastBulkInsertCache.Add(i1); 390 | Check.ThatCode(() => { FastBulkInsertCache.Add(i1); }).Throws(); 391 | var got = FastBulkInsertCache.Get(); 392 | Check.That(got).Equals(i1); 393 | } 394 | 395 | [Case] 396 | public static void TestPropertyNameGeneration() 397 | { 398 | var ins = FastBulkInsertUtil.CreateBulkInserter( 399 | TableMappingRules.UpperSnake("MY_GEN_NAME")); 400 | Check.That(ins.TableName).Equals("MY_GEN_NAME"); 401 | var f = new Fixture(); 402 | var ds = f.CreateMany().ToArray(); 403 | var instr = ins.BuildInstructionsForRows(ds); 404 | Check.That(instr).HasSize(4); 405 | Check.That(instr[0].DbParamType).Equals(DbParameterTypeNumbers.Raw); 406 | Check.That(instr[1].DbParamType).Equals(DbParameterTypeNumbers.NVarChar); 407 | Check.That(instr[2].DbParamType).Equals(DbParameterTypeNumbers.Date); 408 | // enum is number 409 | Check.That(instr[3].DbParamType).Equals(DbParameterTypeNumbers.Number); 410 | 411 | } 412 | 413 | [Case] 414 | public static void TestHasher() 415 | { 416 | 417 | var opts = new FieldHasherOptions 418 | { 419 | ZeroIfNulls = true, 420 | StringNormalizer = (e) => 421 | { 422 | var trim = typeof(string).GetMethod("Trim", new Type[] { }); 423 | var call = Expression.Call(e, trim); 424 | return call; 425 | } 426 | }; 427 | var h = new FieldHasher(ReflectionHelper.GetProps(), opts); 428 | var o = new C(); 429 | var l = new List(); 430 | 431 | int Compute() 432 | { 433 | var val = h.ComputeHash(o); 434 | l.Add(val); 435 | return val; 436 | } 437 | 438 | o.a = 1; 439 | Compute(); 440 | o.mynullable = SomeDate; 441 | Compute(); 442 | 443 | Check.That(l).ContainsExactly(0, 0); 444 | l.Clear(); 445 | 446 | o.s = "notnull"; 447 | Compute(); 448 | 449 | 450 | // now it starts giving nonzero 451 | Check.That(l[0]).IsNotZero(); 452 | o.s = "t"; 453 | Compute(); 454 | o.b = 1; 455 | Compute(); 456 | o.date = SomeDate; 457 | Compute(); 458 | Check.That(l.Distinct()).HasSize(l.Count); 459 | 460 | o.s = " hello "; 461 | var hashWithSpaces = Compute(); 462 | o.s = "hello"; 463 | var hashWithoutSpaces = Compute(); 464 | Check.That(hashWithSpaces).Equals(hashWithoutSpaces); 465 | } 466 | 467 | [Case] 468 | public static void TestOwnPropertyRules() 469 | { 470 | var rules = new TableMappingRules 471 | { 472 | TableName = "MY_TAB", 473 | ColumnNameGenerator = pi => 474 | { 475 | if (pi.Name == "MyString") 476 | { 477 | // strip this out 478 | return null; 479 | } 480 | var guessed = TableMappingRules.GuessColumnNameBasedOnPropertyInfo(pi); 481 | return guessed == "MY_DATE" ? "MY_CHANGED_DATE" : guessed; 482 | } 483 | }; 484 | var ins = 485 | FastBulkInsertUtil.CreateBulkInserter( 486 | rules); 487 | 488 | Check.That(ins.TableName).Equals("MY_TAB"); 489 | Check.That(ins.Properties).HasSize(3); 490 | Check.That(ins.Properties[1].DbColumnName).Equals("MY_CHANGED_DATE"); 491 | } 492 | 493 | [Case] 494 | public static void TestCopyNullableOverNonnullable() 495 | { 496 | var src = new D(); 497 | var dest = new C(); 498 | var copier = new FieldCopier(new[] {"sometimesnullable"}); 499 | src.sometimesnullable = null; 500 | copier.Copy(dest, src); 501 | Check.That(dest.sometimesnullable).Equals(0); 502 | src.sometimesnullable = 12; 503 | copier.Copy(dest, src); 504 | Check.That(dest.sometimesnullable).Equals(12); 505 | src.sometimesnullable = null; 506 | copier.Copy(dest, src); 507 | Check.That(dest.sometimesnullable).Equals(0); 508 | } 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/TestDataPocClass.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace FakePoc { 3 | public class BigDto { 4 | public int i0 { get; set; } = 0; 5 | public int i1 { get; set; } = 1; 6 | public int i2 { get; set; } = 2; 7 | public int i3 { get; set; } = 3; 8 | public int i4 { get; set; } = 4; 9 | public int i5 { get; set; } = 5; 10 | public int i6 { get; set; } = 6; 11 | public int i7 { get; set; } = 7; 12 | public int i8 { get; set; } = 8; 13 | public int i9 { get; set; } = 9; 14 | public int i10 { get; set; } = 10; 15 | public int i11 { get; set; } = 11; 16 | public int i12 { get; set; } = 12; 17 | public int i13 { get; set; } = 13; 18 | public int i14 { get; set; } = 14; 19 | public int i15 { get; set; } = 15; 20 | public int i16 { get; set; } = 16; 21 | public int i17 { get; set; } = 17; 22 | public int i18 { get; set; } = 18; 23 | public int i19 { get; set; } = 19; 24 | public string s0 { get; set; } = "0"; 25 | public string s1 { get; set; } = "1"; 26 | public string s2 { get; set; } = "2"; 27 | public string s3 { get; set; } = "3"; 28 | public string s4 { get; set; } = "4"; 29 | public string s5 { get; set; } = "5"; 30 | public string s6 { get; set; } = "6"; 31 | public string s7 { get; set; } = "7"; 32 | public string s8 { get; set; } = "8"; 33 | public string s9 { get; set; } = "9"; 34 | public string s10 { get; set; } = "10"; 35 | public string s11 { get; set; } = "11"; 36 | public string s12 { get; set; } = "12"; 37 | public string s13 { get; set; } = "13"; 38 | public string s14 { get; set; } = "14"; 39 | public string s15 { get; set; } = "15"; 40 | public string s16 { get; set; } = "16"; 41 | public string s17 { get; set; } = "17"; 42 | public string s18 { get; set; } = "18"; 43 | public string s19 { get; set; } = "19"; 44 | public decimal d0 { get; set; } = 0.0m; 45 | public decimal d1 { get; set; } = 1.1m; 46 | public decimal d2 { get; set; } = 2.2m; 47 | public decimal d3 { get; set; } = 3.3m; 48 | public decimal d4 { get; set; } = 4.4m; 49 | public decimal d5 { get; set; } = 5.5m; 50 | public decimal d6 { get; set; } = 6.6m; 51 | public decimal d7 { get; set; } = 7.7m; 52 | public decimal d8 { get; set; } = 8.8m; 53 | public decimal d9 { get; set; } = 9.9m; 54 | public decimal d10 { get; set; } = 10.10m; 55 | public decimal d11 { get; set; } = 11.11m; 56 | public decimal d12 { get; set; } = 12.12m; 57 | public decimal d13 { get; set; } = 13.13m; 58 | public decimal d14 { get; set; } = 14.14m; 59 | public decimal d15 { get; set; } = 15.15m; 60 | public decimal d16 { get; set; } = 16.16m; 61 | public decimal d17 { get; set; } = 17.17m; 62 | public decimal d18 { get; set; } = 18.18m; 63 | public decimal d19 { get; set; } = 19.19m; 64 | 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/TestDataPocClass.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ import namespace="System.Linq" #> 4 | <#@ import namespace="System.Text" #> 5 | <#@ import namespace="System.Collections.Generic" #> 6 | <#@ output extension=".cs" #> 7 | 8 | namespace FakePoc { 9 | class BigDto { 10 | <# for (var i = 0; i < 20; i++) { #> 11 | public int i<#=i#> { get; set; } = <#=i#>; 12 | <# } #> 13 | <# for (var i = 0; i < 20; i++) { #> 14 | public string s<#=i#> { get; set; } = "<#=i#>"; 15 | <# } #> 16 | <# for (var i = 0; i < 20; i++) { #> 17 | public decimal d<#=i#> { get; set; } = <#=i#>.<#=i#>m; 18 | <# } #> 19 | 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/TestEntities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace FastExpressionKit.Integration.Tests 9 | { 10 | [Table("DB_VERSION_INFO")] 11 | public class TestDbEntity 12 | { 13 | [Column("VERSION")] 14 | public Guid MyId { get; set; } 15 | 16 | [Column("PRODUCT")] 17 | public string MyString { get; set; } 18 | 19 | [Column("UPDATE_TIME")] 20 | public DateTime MyDate { get; set; } 21 | 22 | } 23 | 24 | public enum SomeEnum 25 | { 26 | One = 1, 27 | Three = 3 28 | } 29 | 30 | public class SomeOtherEntity 31 | { 32 | public string Id { get; set; } 33 | } 34 | public class TestDbEntityWithoutAnnotations 35 | { 36 | public Guid MyId { get; set; } 37 | public string MyString { get; set; } 38 | // this collection will be skipped 39 | public ICollection SomeCollection { 40 | get; 41 | set; 42 | } 43 | 44 | public DateTime MyDate { get; set; } 45 | public SomeOtherEntity NavigationProp { get; set; } 46 | 47 | public SomeEnum MyEnum { get; set; } 48 | 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /FastExpressionKit.Test/TrivialValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace FastExpressionKit.Test 7 | { 8 | public class ValidationError : Exception 9 | { 10 | public ValidationError(string s) : base(s) 11 | { 12 | } 13 | 14 | } 15 | /// 16 | /// Use this to register very simple string validator that checks all string properties for "bad" characters 17 | /// 18 | /// Minimal customization is offered 19 | /// - Set your own BadCharacters array (static) to use globally 20 | /// - Configure the property list to check with AddValidator to exclude some properties you don't want to check 21 | /// 22 | /// Design goal: be fast to run 23 | /// 24 | /// 25 | public static class TrivialValidator 26 | { 27 | // yes, it's public by design 28 | 29 | public static char[] BadCharacters = new [] { '\\', '\'', '<', '>', '"', '&' }; 30 | private static Dictionary> validatorCache = new Dictionary>(); 31 | public static void ValidateString(Type t, string key, string value, char[] badChars) 32 | { 33 | var loc = value.IndexOfAny(badChars); 34 | if (loc != -1) 35 | throw new ValidationError($"Type: {t.ToString()}, Property {key} contained illegal character '{value[loc]}' at position {loc}"); 36 | } 37 | 38 | public static void SetValidator(Func shouldValidateProperty, char[] badChars, bool overwrite = false, bool compileNow = false) 39 | { 40 | if (!overwrite && validatorCache.ContainsKey(typeof(T))) 41 | return; 42 | var props = ReflectionHelper.GetProps() 43 | .Where(p => p.PropertyType == typeof(string) && shouldValidateProperty(p)) 44 | .Select(p => p.Name).ToList(); 45 | 46 | var lazyEntry = new Lazy(() => 47 | { 48 | var validator = 49 | new RunMethodForEachProperty(props, 50 | typeof(TrivialValidator).GetMethod(nameof(ValidateString)), 51 | badChars); 52 | return validator; 53 | }); 54 | validatorCache[typeof(T)] = lazyEntry; 55 | if (compileNow) 56 | { 57 | _ = lazyEntry.Value; 58 | } 59 | 60 | } 61 | 62 | public static void Validate(T obj) 63 | { 64 | var validator = validatorCache[typeof(T)].Value; 65 | var typed = validator as RunMethodForEachProperty; 66 | typed.Run(obj); 67 | } 68 | 69 | public static void ValidateMany(IEnumerable items) 70 | { 71 | int idx = 0; 72 | foreach (var item in items) 73 | { 74 | try 75 | { 76 | Validate(item); 77 | idx++; 78 | } catch (ValidationError err) 79 | { 80 | throw new ValidationError($"ValidateMany failed at index {idx}: {err.Message}"); 81 | } 82 | 83 | } 84 | } 85 | 86 | } 87 | } -------------------------------------------------------------------------------- /FastExpressionKit.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29806.167 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{D7A5DAB8-1D8D-43C2-BEEF-5DD5B5855C6E}" 7 | ProjectSection(SolutionItems) = preProject 8 | paket.dependencies = paket.dependencies 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastExpressionKit", "FastExpressionKit\FastExpressionKit.csproj", "{71FDACFF-C5CF-4B31-BB3B-6668E1A3672C}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastExpressionKit.Test", "FastExpressionKit.Test\FastExpressionKit.Test.csproj", "{1A357FA1-8AF9-410A-848D-03568F4B9825}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0299390A-90A6-4A65-BB4C-4A7EE796C44A}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastExpressionKit.BulkInsert", "FastExpressionKit.BulkInsert\FastExpressionKit.BulkInsert.csproj", "{3E6E76E7-4817-44F8-A28C-B78E3BD4C8FF}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FastExpressionKit.Integration.Tests", "FastExpressionKit.Integration.Tests\FastExpressionKit.Integration.Tests.csproj", "{0374E33E-EC15-482F-9F8D-8A4290990D4C}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {71FDACFF-C5CF-4B31-BB3B-6668E1A3672C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {71FDACFF-C5CF-4B31-BB3B-6668E1A3672C}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {71FDACFF-C5CF-4B31-BB3B-6668E1A3672C}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {71FDACFF-C5CF-4B31-BB3B-6668E1A3672C}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {1A357FA1-8AF9-410A-848D-03568F4B9825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {1A357FA1-8AF9-410A-848D-03568F4B9825}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {1A357FA1-8AF9-410A-848D-03568F4B9825}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {1A357FA1-8AF9-410A-848D-03568F4B9825}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {3E6E76E7-4817-44F8-A28C-B78E3BD4C8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {3E6E76E7-4817-44F8-A28C-B78E3BD4C8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {3E6E76E7-4817-44F8-A28C-B78E3BD4C8FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {3E6E76E7-4817-44F8-A28C-B78E3BD4C8FF}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {0374E33E-EC15-482F-9F8D-8A4290990D4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {0374E33E-EC15-482F-9F8D-8A4290990D4C}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {0374E33E-EC15-482F-9F8D-8A4290990D4C}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {0374E33E-EC15-482F-9F8D-8A4290990D4C}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | GlobalSection(SolutionProperties) = preSolution 45 | HideSolutionNode = FALSE 46 | EndGlobalSection 47 | GlobalSection(ExtensibilityGlobals) = postSolution 48 | SolutionGuid = {92AF36B8-EB91-4610-9019-08269645F8B5} 49 | EndGlobalSection 50 | EndGlobal 51 | -------------------------------------------------------------------------------- /FastExpressionKit/FastExpressionKit.cs: -------------------------------------------------------------------------------- 1 | // FastExpressionKit 2 | // License: MIT: Copyright (c) 2017 Ville M. Vainio 3 | // See details at https://github.com/vivainio/FastExpressionKit 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Linq.Expressions; 10 | using System.Reflection; 11 | 12 | namespace FastExpressionKit 13 | { 14 | using DifferReturnValueType = Tuple; 15 | 16 | public static class EE 17 | { 18 | public static ParameterExpression Var(string name) => Expression.Variable(typeof(T), name); 19 | public static ParameterExpression Param(string name) => Expression.Parameter(typeof(T), name); 20 | public static MemberExpression Dot(this Expression exp, string fieldName) => Expression.PropertyOrField(exp, fieldName); 21 | public static Expression IsEq(this Expression left, Expression right) => Expression.Equal(left, right); 22 | public static Expression Val(T value) => Expression.Constant(value, typeof(T)); 23 | 24 | public static Expression Mul(this Expression left, int right) => Expression.Multiply(left, Expression.Constant(right)); 25 | public static Expression MulAssign(this Expression left, int right) => Expression.MultiplyAssign(left, Expression.Constant(right)); 26 | 27 | public static Expression Add(this Expression left, int right) => Expression.Add(left, Expression.Constant(right)); 28 | public static Expression Add(this Expression left, Expression right) => Expression.Add(left, right); 29 | 30 | public static Expression Mul(this Expression left, Expression right) => Expression.Multiply(left, right); 31 | public static Expression Let(this Expression left, Expression right) => Expression.Assign(left, right); 32 | public static MethodInfo Method(Expression exp) => 33 | (exp.Body as MethodCallExpression).Method; 34 | public static Expression Box(Expression e) => Expression.Convert(e, typeof(object)); 35 | public static Expression NotNull(this MemberExpression left) => Expression.NotEqual(left, Expression.Constant(null)); 36 | public static Expression Call(this Expression left, string methodName) => 37 | Expression.Call(left, typeof(object).GetMethod(methodName) ); 38 | } 39 | 40 | public class Differ 41 | { 42 | public readonly string[] Props; 43 | private readonly Func comparisonFunc; 44 | private Func CreateExpression(IEnumerable fields) 45 | { 46 | var t1param = EE.Param("left"); 47 | var t2param = EE.Param("right"); 48 | var NULL = EE.Val(null); 49 | var TupleCreate = EE.Method(() => Tuple.Create("", null)); 50 | var cmplist2 = fields.Select(f => 51 | Expression.Condition(t1param.Dot(f).IsEq(t2param.Dot(f)), 52 | NULL, 53 | Expression.Call(TupleCreate, EE.Val(f), EE.Box(t2param.Dot(f))))); 54 | var resultArr = Expression.NewArrayInit(typeof(DifferReturnValueType), cmplist2); 55 | 56 | var l = Expression.Lambda>(resultArr, t1param, t2param); 57 | return l.Compile(); 58 | } 59 | 60 | public Differ(string[] props) 61 | { 62 | this.Props = props; 63 | this.comparisonFunc = CreateExpression(props); 64 | } 65 | 66 | public DifferReturnValueType[] Compare(T1 left, T2 right) => comparisonFunc.Invoke(left, right); 67 | } 68 | 69 | public class FieldCopier 70 | { 71 | public readonly string[] Props; 72 | private readonly Action assignExpr; 73 | 74 | private Action CreateExpression(IEnumerable fields) 75 | { 76 | var targetParam = EE.Param("left"); 77 | var sourceParam = EE.Param("right"); 78 | 79 | IEnumerable assignList = fields.Select(f => 80 | { 81 | var propTarget = typeof(TTarget).GetProperty(f); 82 | var propSource = typeof(TSrc).GetProperty(f); 83 | if (propTarget.PropertyType == propSource.PropertyType) 84 | { 85 | return Expression.Assign(targetParam.Dot(f), sourceParam.Dot(f)) as Expression; 86 | } 87 | else if (propTarget.PropertyType == typeof(int) && propSource.PropertyType.IsEnum || 88 | propSource.PropertyType == typeof(int) && propTarget.PropertyType.IsEnum) 89 | { 90 | return Expression.Assign(targetParam.Dot(f), Expression.Convert(sourceParam.Dot(f), propTarget.PropertyType)); 91 | } 92 | // assigning nullable to non-nullable creates prop = default(T) 93 | else if (Nullable.GetUnderlyingType(propSource.PropertyType) == propTarget.PropertyType) 94 | { 95 | var iffed = Expression.IfThenElse(sourceParam.Dot(f).NotNull(), 96 | Expression.Assign(targetParam.Dot(f), 97 | Expression.Convert(sourceParam.Dot(f), propTarget.PropertyType)), 98 | Expression.Assign(targetParam.Dot(f), Expression.Default(propTarget.PropertyType))); 99 | return iffed; 100 | 101 | } 102 | // target is nullable, source is not - nothing special, it will never get null 103 | else if (Nullable.GetUnderlyingType(propTarget.PropertyType) == propSource.PropertyType) 104 | { 105 | return Expression.Assign(targetParam.Dot(f), Expression.Convert(sourceParam.Dot(f), propTarget.PropertyType)); 106 | } 107 | 108 | throw new InvalidOperationException($"Invalid operation, cannot copy {propSource} to {propTarget}."); 109 | }); 110 | //var resultArr = Expression.NewArrayInit(typeof(bool), cmplist); 111 | var block = Expression.Block(assignList); 112 | var l = Expression.Lambda>(block, targetParam, sourceParam); 113 | return l.Compile(); 114 | } 115 | 116 | public FieldCopier(string[] props) 117 | { 118 | this.Props = props; 119 | this.assignExpr = CreateExpression(props); 120 | } 121 | 122 | public void Copy(TTarget left, TSrc right) => assignExpr.Invoke(left, right); 123 | } 124 | 125 | public class FieldHasherOptions 126 | { 127 | public Func StringNormalizer { get; set; } 128 | // early evaluate to zero if any nulls found 129 | public bool ZeroIfNulls { get; set; } = false; 130 | 131 | } 132 | public class FieldHasher 133 | { 134 | public readonly PropertyInfo[] Props; 135 | 136 | private readonly Func expr; 137 | 138 | private Func CreateExpression(PropertyInfo[] fields, FieldHasherOptions options) 139 | { 140 | /* 141 | $hash = 17; 142 | $hash += $hash * 23 + .Call ($obj.a).GetHashCode(); 143 | $hash += $hash * 23 + .Call ($obj.b).GetHashCode(); 144 | .If ($obj.s != null) { 145 | $hash += $hash * 23 + .Call (.Call ($obj.s).Trim()).GetHashCode() 146 | } .Else { 147 | $hash *= 23 148 | }; 149 | $hash += $hash * 23 + .Call ($obj.date).GetHashCode(); 150 | $hash += $hash * 23 + .Call ($obj.mynullable).GetHashCode(); 151 | $hash 152 | */ 153 | const int prim = 17; 154 | const int prim2 = 23; 155 | var t1param = EE.Param("obj"); 156 | var resultHash = EE.Param("hash"); 157 | var ass = Expression.Assign(resultHash, Expression.Constant(prim)); 158 | var block = new List(); 159 | block.Add(ass); 160 | var returnLabel = Expression.Label(typeof(int)); 161 | 162 | var whenNull = options.ZeroIfNulls ? Expression.Return(returnLabel, Expression.Constant(0)) 163 | : resultHash.MulAssign(prim2); 164 | 165 | block.AddRange(fields.Select(pi => 166 | { 167 | var dotted = t1param.Dot(pi.Name); 168 | var propType = pi.PropertyType; 169 | 170 | var addAssignHashCode = Expression.AddAssign(resultHash, 171 | resultHash.Mul(prim2).Add(dotted.Call("GetHashCode"))); 172 | 173 | // nullable 174 | if (options.ZeroIfNulls && Nullable.GetUnderlyingType(propType) != null) 175 | { 176 | return Expression.IfThenElse( 177 | dotted.NotNull(), 178 | addAssignHashCode, 179 | whenNull); 180 | } 181 | 182 | // value type but not nullabl 183 | if (propType.IsValueType) 184 | { 185 | return (Expression) addAssignHashCode; 186 | } 187 | 188 | 189 | // normalized string 190 | var normalized = propType == typeof(string) && options.StringNormalizer != null 191 | ? options.StringNormalizer(dotted) 192 | : dotted; 193 | 194 | var iffed = Expression.IfThenElse(dotted.NotNull(), 195 | Expression.AddAssign(resultHash, 196 | resultHash.Mul(prim2).Add(normalized.Call("GetHashCode"))), 197 | whenNull); 198 | 199 | return iffed; 200 | })); 201 | 202 | block.Add(Expression.Label(returnLabel, resultHash)); 203 | var eblock = Expression.Block(new [] { resultHash} , block); 204 | var l = Expression.Lambda>(eblock, t1param); 205 | return l.Compile(); 206 | } 207 | 208 | public FieldHasher(PropertyInfo[] props) 209 | { 210 | this.Props = props; 211 | this.expr = CreateExpression(props, null); 212 | } 213 | public FieldHasher(PropertyInfo[] props, FieldHasherOptions options) 214 | { 215 | this.Props = props; 216 | this.expr = CreateExpression(props, options); 217 | } 218 | public int ComputeHash(T1 obj) => unchecked(expr.Invoke(obj)); 219 | 220 | } 221 | 222 | /// 223 | /// Run some static method for each property in the list 224 | /// 225 | /// Not quite typesafe. All the properties listed should be of same type 226 | /// 227 | /// 228 | public class RunMethodForEachProperty 229 | { 230 | public readonly string[] Props; 231 | private readonly Action expr; 232 | 233 | public RunMethodForEachProperty(IEnumerable fields, MethodInfo methodToCall, TExtra extraParam) 234 | { 235 | expr = CreateExpression(fields, methodToCall, extraParam); 236 | } 237 | 238 | public void Run(T1 obj) 239 | { 240 | expr(obj); 241 | } 242 | private Action CreateExpression(IEnumerable fields, MethodInfo methodToCall, TExtra extraParam) 243 | { 244 | var t1param = EE.Param("obj"); 245 | 246 | List calls = new List(); 247 | foreach (var field in fields) 248 | { 249 | // the call will always be with (type, key, value, EXTRA) 250 | var value = t1param.Dot(field); 251 | var nameConstant = Expression.Constant(field); 252 | var typeArg = Expression.Constant(typeof(T1)); 253 | var extraConstant = Expression.Constant(extraParam); 254 | var call = Expression.Call(methodToCall, typeArg, nameConstant, value, extraConstant); 255 | calls.Add(call); 256 | } 257 | var res = Expression.Block(calls); 258 | var l = Expression.Lambda>(res, t1param); 259 | return l.Compile(); 260 | } 261 | } 262 | public class FieldExtract 263 | { 264 | public readonly string[] Props; 265 | private readonly Func expr; 266 | 267 | private Func CreateExpression(IEnumerable fields) 268 | { 269 | var t1param = EE.Param("obj"); 270 | var elist = fields.Select(f => t1param.Dot(f)).Cast(); 271 | if (typeof(TVal) == typeof(object)) 272 | elist = elist.Select(e => EE.Box(e)); 273 | var resultArr = Expression.NewArrayInit(typeof(TVal), elist); 274 | var l = Expression.Lambda>(resultArr, t1param); 275 | return l.Compile(); 276 | } 277 | 278 | public FieldExtract(string[] props) 279 | { 280 | this.Props = props; 281 | this.expr = CreateExpression(props); 282 | } 283 | 284 | public TVal[] Extract(T1 obj) => expr.Invoke(obj); 285 | 286 | // zip {} 287 | public IEnumerable> ResultsAsZip(ICollection hits) 288 | { 289 | var r = Enumerable.Zip(Props, hits, (p, h) => Tuple.Create(p, h)); 290 | return r; 291 | } 292 | 293 | // hits can be any enumerable, as long as it can be zipped with Props 294 | // 295 | public Dictionary ResultsAsDict(ICollection hits) 296 | { 297 | var d = new Dictionary(); 298 | for (var i = 0; i < hits.Count; i++) 299 | { 300 | d[Props[i]] = hits.ElementAt(i); 301 | } 302 | return d; 303 | } 304 | } 305 | // Usable with FieldExtract instances 306 | public static class FieldExtractUtil 307 | { 308 | // emit array of object arrays, usable e.g. for sql bulk copy (one array per extracted property) 309 | public static object[][] ExtractToObjectArrays(FieldExtract extractor, IReadOnlyList entries) 310 | { 311 | // extract everything to jagged arrays 312 | // then transpose it 313 | var ecount = entries.Count; 314 | var extracted = new object[entries.Count][]; 315 | for (var i = 0; i < entries.Count; i++) 316 | { 317 | extracted[i] = extractor.Extract(entries.ElementAt(i)); 318 | 319 | } 320 | object[][] jagged = new object[extractor.Props.Length][]; 321 | for (var i = 0; i < extractor.Props.Length; i++) 322 | { 323 | jagged[i] = new object[ecount]; 324 | 325 | } 326 | 327 | for (var i = 0; i < entries.Count; i++) 328 | { 329 | for (var j = 0; j < extractor.Props.Count(); j++) 330 | { 331 | jagged[j][i] = extracted[i][j]; 332 | } 333 | 334 | } 335 | return jagged; 336 | } 337 | } 338 | public static class ReflectionHelper 339 | { 340 | // create cache for GetExtractorFor by reflecting on object 341 | public static PropertyInfo[] GetProps() => typeof(T) 342 | .GetProperties(BindingFlags.Instance | BindingFlags.Public); 343 | public static PropertyInfo[] GetPropsByNames(IEnumerable propNames) => 344 | propNames.Select(i => typeof(T).GetProperty(i)).ToArray(); 345 | 346 | public static string[] PropNames() => GetProps().Select(p => p.Name).ToArray(); 347 | public static string[] WriteablePropNames() => GetProps().Where(p => p.CanWrite).Select(p => p.Name).ToArray(); 348 | public static IEnumerable> CollectProps() => 349 | GetProps() 350 | .GroupBy(prop => prop.PropertyType) 351 | .Select(g => Tuple.Create(g.Key, g.Select(pi => pi.Name).ToArray())); 352 | 353 | // use after CollectProps 354 | public static FieldExtract GetExtractorFor(IEnumerable> propsCollection) 355 | { 356 | var proplist = propsCollection.First(el => el.Item1 == typeof(T2)); 357 | if (proplist == null) 358 | return null; 359 | return new FieldExtract(proplist.Item2); 360 | } 361 | 362 | } 363 | 364 | } -------------------------------------------------------------------------------- /FastExpressionKit/FastExpressionKit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | FastExpressionKit 6 | Library to reduce repeated reflection 7 | vivainio 8 | 9 | Ville Vainio 10 | https://github.com/vivainio/FastExpressionKit 11 | https://github.com/vivainio/FastExpressionKit/blob/master/LICENSE 12 | 13 | 14 | true 15 | ../keypair.snk 16 | 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ville Vainio 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 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastExpressionKit 2 | 3 | ![.NET Core](https://github.com/vivainio/FastExpressionKit/workflows/.NET%20Core/badge.svg) 4 | 5 | ## A small library to make reflection-y things faster 6 | 7 | Reflection in C# can be slow. There are operations that iterate over certain properties of objects that typically use reflection, with suboptimal performance. 8 | 9 | This mini-library (just one .cs file to copy to your project, less than 200 lines!) provides minimal building blocks that generate code (Linq Expressions) and compile it to fast machine code. The recommended pattern is to instantiate the class where you configure your mappings (takes few milliseconds), and then reuse the instance for hundreds of operations with good performance (few microseconds per round). 10 | 11 | ## Installation 12 | 13 | Options: 14 | 15 | Either 16 | 17 | - Download FastExpressionKit.cs and add it to your project 18 | 19 | Or use the nuget package: 20 | 21 | ``` 22 | $ dotnet add package FastExpressionKit 23 | ``` 24 | 25 | and to refer to the file in your csproj file, add this to `paket.references`: 26 | 27 | ``` 28 | File: FastExpressionKit.cs 29 | ``` 30 | 31 | After `paket install`, you will find the file in `paket-files/` directory. A reference has automatically been added to your csproj file. 32 | 33 | 34 | 35 | ## Usage 36 | 37 | ### FieldExtract 38 | 39 | Extract fields a and b to array of integers: 40 | 41 | ```csharp 42 | var extractor = new FieldExtract(new[] { "a", "b" };); 43 | var results = extractor.Extract(c1); 44 | ``` 45 | 46 | If you want fields boxed to object (i.e. don't want to specify the target type), there is a special case for object: 47 | 48 | ```csharp 49 | var boxedExtract = new FieldExtract(bigpropnames); 50 | ``` 51 | 52 | Clearly, dealing with the end result will be slower as you got objects now. 53 | 54 | ### Differ 55 | 56 | Compare two objects field-by-field, yielding a list of differences. Works across different classes. 57 | 58 | ```csharp 59 | var differ = new Differ(new[] { "a", "b" }); 60 | var res = differ.Compare(c1, c2); 61 | 62 | // compare different types! 63 | var differ2 = new Differ(new[] { "a", "b" }); 64 | res = differ2.Compare(c1, d1); 65 | ``` 66 | 67 | ### Copier 68 | 69 | Copy a set of fields from one object to another. 70 | 71 | 72 | ```csharp 73 | var copier = new FieldCopier(fields); 74 | copier.Copy(c1, c2); 75 | ``` 76 | 77 | Yes, this is essentially a trivial version of AutoMapper (when you specify the list of fields in the target type). 78 | The property types must be of the same type, as copying is just doing src.foo = target.foo for each property. 79 | 80 | Nullable T? can be assigned on top of non-nullable T with FieldCopier, it coerces nulls to default(T). 81 | 82 | ### Utility classes 83 | 84 | The library also contains some helpers for doing reflection (ReflectionHelper) and static helpers for creating 85 | Expressions (EE), but those are not part of the documented API. 86 | 87 | ### License 88 | 89 | ``` 90 | License: MIT 91 | Copyright 2017 Ville M. Vainio 92 | ``` 93 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 2 | # Build and test ASP.NET Core projects targeting .NET Core. 3 | # Add steps that run tests, create a NuGet package, deploy, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core 5 | 6 | trigger: 7 | - master 8 | 9 | pool: 10 | vmImage: 'Ubuntu-16.04' 11 | 12 | variables: 13 | buildConfiguration: 'Release' 14 | 15 | steps: 16 | - script: dotnet build --configuration $(buildConfiguration) 17 | displayName: 'dotnet build $(buildConfiguration)' 18 | 19 | - script: dotnet run 20 | displayName: 'Unit tests' 21 | workingDirectory: FastExpressionKit.Test 22 | 23 | -------------------------------------------------------------------------------- /keypair.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vivainio/FastExpressionKit/3488404f07cf5525872a0aab6c69e749a89bbd52/keypair.snk -------------------------------------------------------------------------------- /publish.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | from pathlib import Path 4 | 5 | import os, shutil 6 | 7 | LOCAL_FEED = Path(r".\dev\localnuget").absolute() 8 | 9 | if not LOCAL_FEED.exists(): 10 | os.makedirs(LOCAL_FEED) 11 | 12 | projects = ["FastExpressionKit", "FastExpressionKit.BulkInsert"] 13 | version = "1.5.0" 14 | 15 | 16 | def c(s): 17 | print(">", s) 18 | err = os.system(s) 19 | assert not err 20 | 21 | 22 | def nuke(pth): 23 | if os.path.isdir(pth): 24 | shutil.rmtree(pth) 25 | 26 | 27 | def nuget_add(pth): 28 | c(f"dotnet nuget push -s {LOCAL_FEED} {pth}") 29 | 30 | 31 | startdir = Path(".").absolute() 32 | 33 | for prjdir in projects: 34 | os.chdir(startdir / prjdir) 35 | nuke("bin") 36 | nuke("obj") 37 | 38 | def pack(): 39 | c(f"dotnet pack -c Release /p:Version={version} /p:PubVer={version}") 40 | pkgs = list(Path("bin/Release").glob("*.nupkg")) 41 | nuget_add(pkgs[0]) 42 | 43 | pack() 44 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | 4 | os.environ["DOTNET_NOLOGO"] = "true" 5 | LOCAL_NUGET = "dev/localnuget" 6 | if not os.path.isdir(LOCAL_NUGET): 7 | os.makedirs(LOCAL_NUGET) 8 | os.chdir("FastExpressionKit.Test") 9 | subprocess.check_call(["dotnet", "run"]) --------------------------------------------------------------------------------