├── .editorconfig ├── .gitignore ├── Artisan.Orm ├── Artisan.Orm.csproj ├── DataReply.cs ├── DataReplyException.cs ├── DataReplyMessage.cs ├── DataReplyState.cs ├── DataTableExtensions.cs ├── INode.cs ├── LICENSE.txt ├── Logo.ico ├── MapperForAttribute.cs ├── MappingManager.cs ├── MergeJoinExtensions.cs ├── NegativeIdentity.cs ├── ObjectRows.cs ├── RepositoryBase.cs ├── SqlCommandExtensions_Async.cs ├── SqlCommandExtensions_Parameters.cs ├── SqlCommandExtensions_Sync.cs ├── SqlDataReaderExtensions_CreateObject.cs ├── SqlDataReaderExtensions_Get.cs ├── SqlDataReaderExtensions_Read.cs ├── StringExtensions.cs ├── TreeExtensions.cs └── TypeExtensions.cs ├── Artisan.sln ├── Database ├── 1. Tables │ ├── Records │ │ ├── 1. GrandRecords.sql │ │ ├── 2. Records.sql │ │ ├── 3. ChildRecords.sql │ │ └── 4. RecordTypes.sql │ └── Users │ │ ├── 1. Users.sql │ │ ├── 2. Roles.sql │ │ ├── 3. UserRoles.sql │ │ └── 4. Folders.sql ├── 2. Views │ ├── vwChildRecords.sql │ ├── vwGrandRecords.sql │ ├── vwRecords.sql │ ├── vwRecordsWithTypes.sql │ └── vwUsers.sql ├── 3. Functions │ ├── GetHidCode.sql │ ├── GetUserRoleIds.sql │ └── Utility Functions │ │ ├── GetErrorMessage.sql │ │ ├── SplitIntIds.sql │ │ ├── SplitSmallIntIds.sql │ │ └── SplitTinyIntIds.sql ├── 4. Types │ ├── ChildRecordTableType.sql │ ├── Common │ │ ├── BigIntIdTableType.sql │ │ ├── DataMessageTableType.sql │ │ ├── IntIdTableType.sql │ │ ├── SmallIntIdTableType.sql │ │ └── TinyIntIdTableType.sql │ ├── FolderTableType.sql │ ├── GrandRecordTableType.sql │ ├── RecordTableType.sql │ └── UserTableType.sql ├── 5. Procedures │ ├── Folders │ │ ├── DeleteFolder.sql │ │ ├── FindFoldersWithParents.sql │ │ ├── GenerateFolders.sql │ │ ├── GetFolderById.sql │ │ ├── GetFolderParents.sql │ │ ├── GetFolderWithSubFolders.sql │ │ ├── GetImmediateSubFolders.sql │ │ ├── GetNextSiblingFolder.sql │ │ ├── GetPreviousSiblingFolder.sql │ │ ├── GetUserRootFolder.sql │ │ ├── ReculcSubFolderHids.sql │ │ └── SaveFolder.sql │ ├── GrandRecords │ │ ├── GetGrandRecordById.sql │ │ ├── GetGrandRecords.sql │ │ └── SaveGrandRecords.sql │ ├── Records │ │ ├── GetRecordById.sql │ │ ├── GetRecords.sql │ │ └── SaveRecords.sql │ ├── SqlParameters │ │ ├── GetDateTimeParams.sql │ │ ├── GetFractionalNumberParams.sql │ │ ├── GetGuidAndRowVersionParams.sql │ │ ├── GetStringParams.sql │ │ └── GetWholeNumberParams.sql │ └── Users │ │ ├── DeleteUser.sql │ │ ├── GetUserById.sql │ │ ├── GetUsers.sql │ │ ├── SaveUser.sql │ │ └── SaveUsers.sql ├── 6. Post-Deployment │ ├── 1. Users.sql │ ├── 2. Roles.sql │ ├── 3. UserRoles.sql │ ├── 4. RecordTypes.sql │ ├── 5. Records.sql │ ├── 6. Sequence reset.sql │ └── PostDeployment.sql ├── Artisan.publish.xml ├── Database.sqlproj └── Utility Scripts │ ├── Artisan Code Generation.sql │ └── Generate MERGE SQL.sql ├── Logo.png ├── README.md └── Tests ├── AppSettings.cs ├── DAL ├── ByteArrayConverter.cs ├── Folders │ ├── Models │ │ └── Folder.cs │ └── Repository.cs ├── GrandRecords │ ├── Models │ │ ├── ChildRecord.cs │ │ ├── GrandRecord.cs │ │ ├── Record.cs │ │ └── RecordType.cs │ └── Repository.cs ├── Records │ ├── Models │ │ └── Record.cs │ └── Repository.cs └── Users │ ├── Models │ ├── Role.cs │ └── User.cs │ └── Repository.cs ├── DataServices ├── DataServiceBase.cs └── UserDataService.cs ├── Tests.csproj ├── Tests ├── CommandReadTest.cs ├── EspecialRepositoryTest.cs ├── FolderTests.cs ├── GrandRecordRepositoryTest.cs ├── ReadDictionaryTest.cs ├── RecordRepositoryTest.cs ├── SqlParemeterTest.cs ├── UserDataServiceTest.cs └── UserRepositoryTest.cs └── appsettings.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{cs,sql}] 2 | charset = utf-8 3 | indent_style = tab 4 | indent_size = 4 5 | trim_trailing_whitespace = true 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | *.jfm 73 | 74 | # Chutzpah Test files 75 | _Chutzpah* 76 | 77 | # Visual C++ cache files 78 | ipch/ 79 | *.aps 80 | *.ncb 81 | *.opendb 82 | *.opensdf 83 | *.sdf 84 | *.cachefile 85 | *.VC.db 86 | *.VC.VC.opendb 87 | 88 | # Visual Studio profiler 89 | *.psess 90 | *.vsp 91 | *.vspx 92 | *.sap 93 | 94 | # TFS 2012 Local Workspace 95 | $tf/ 96 | 97 | # Guidance Automation Toolkit 98 | *.gpState 99 | 100 | # ReSharper is a .NET coding add-in 101 | _ReSharper*/ 102 | *.[Rr]e[Ss]harper 103 | *.DotSettings.user 104 | 105 | # JustCode is a .NET coding add-in 106 | .JustCode 107 | 108 | # TeamCity is a build add-in 109 | _TeamCity* 110 | 111 | # DotCover is a Code Coverage Tool 112 | *.dotCover 113 | 114 | # NCrunch 115 | _NCrunch_* 116 | .*crunch*.local.xml 117 | nCrunchTemp_* 118 | 119 | # MightyMoose 120 | *.mm.* 121 | AutoTest.Net/ 122 | 123 | # Web workbench (sass) 124 | .sass-cache/ 125 | 126 | # Installshield output folder 127 | [Ee]xpress/ 128 | 129 | # DocProject is a documentation generator add-in 130 | DocProject/buildhelp/ 131 | DocProject/Help/*.HxT 132 | DocProject/Help/*.HxC 133 | DocProject/Help/*.hhc 134 | DocProject/Help/*.hhk 135 | DocProject/Help/*.hhp 136 | DocProject/Help/Html2 137 | DocProject/Help/html 138 | 139 | # Click-Once directory 140 | publish/ 141 | 142 | # Publish Web Output 143 | # *.[Pp]ublish.xml 144 | *.azurePubxml 145 | # TODO: Comment the next line if you want to checkin your web deploy settings 146 | # but database connection strings (with potential passwords) will be unencrypted 147 | *.pubxml 148 | *.publishproj 149 | 150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 151 | # checkin your Azure Web App publish settings, but sensitive information contained 152 | # in these scripts will be unencrypted 153 | PublishScripts/ 154 | 155 | # NuGet Packages 156 | *.nupkg 157 | *.nuspec 158 | # The packages folder can be ignored because of Package Restore 159 | **/packages/* 160 | # except build/, which is used as an MSBuild target. 161 | !**/packages/build/ 162 | # Uncomment if necessary however generally it will be regenerated when needed 163 | #!**/packages/repositories.config 164 | # NuGet v3's project.json files produces more ignoreable files 165 | *.nuget.props 166 | *.nuget.targets 167 | 168 | # Microsoft Azure Build Output 169 | csx/ 170 | *.build.csdef 171 | 172 | # Microsoft Azure Emulator 173 | ecf/ 174 | rcf/ 175 | 176 | # Windows Store app package directories and files 177 | AppPackages/ 178 | BundleArtifacts/ 179 | Package.StoreAssociation.xml 180 | _pkginfo.txt 181 | 182 | # Visual Studio cache files 183 | # files ending in .cache can be ignored 184 | *.[Cc]ache 185 | # but keep track of directories ending in .cache 186 | !*.[Cc]ache/ 187 | 188 | # Others 189 | ClientBin/ 190 | ~$* 191 | *~ 192 | *.dbmdl 193 | *.dbproj.schemaview 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | Artisan.Orm/1. How-to-publish-NuGet-package.txt 257 | desktop.ini 258 | Artisan.Orm/nuget.exe 259 | -------------------------------------------------------------------------------- /Artisan.Orm/Artisan.Orm.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | disable 6 | True 7 | Artisan.ORM 8 | 3.5.1 9 | Vadim Loboda 10 | 11 | ADO.NET Micro-ORM to SQL Server, implemented as .NET Standard 2.1 (since version 3.5.x) or a .Net Core 6.0 library (since version 3.0.0). 12 | Use version 2.x.x, which was built with Net Standard 2.0, if you want to utilise this library with the .Net Framework or a previous version of .NET Core. 13 | 14 | This library is designed to use stored procedures, table-valued parameters and structured static mappers, with the goal of reading and saving of complex object graphs at once in the fast, convinient and efficient way. 15 | 16 | Read more: https://www.codeproject.com/articles/1155836/artisan-orm-or-how-to-reinvent-the-wheel 17 | 18 | Copyright 2016-2025 19 | https://github.com/lobodava/artisan-orm 20 | Logo.png 21 | https://github.com/lobodava/artisan-orm 22 | Git 23 | ado.net orm micro-orm sql server mssql 24 | Added support for CancellationToken in asynchronous methods. 25 | Upgraded the Microsoft.Data.SqlClient package to version 5.2.3. 26 | LICENSE.txt 27 | Logo.ico 28 | Artisan.ORM 29 | Artisan.ORM 30 | README.md 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | True 41 | \ 42 | 43 | 44 | True 45 | \ 46 | 47 | 48 | True 49 | \ 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /Artisan.Orm/DataReply.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using static System.String; 4 | 5 | namespace Artisan.Orm 6 | { 7 | [DataContract] 8 | public class DataReply { 9 | 10 | [DataMember] 11 | public DataReplyStatus Status { get; set; } 12 | 13 | [DataMember(EmitDefaultValue = false)] 14 | public DataReplyMessage[] Messages { get; set; } 15 | 16 | public DataReply() 17 | { 18 | Status = DataReplyStatus.Ok; 19 | Messages = null; 20 | } 21 | 22 | public DataReply(DataReplyStatus status) 23 | { 24 | Status = status; 25 | Messages = null; 26 | } 27 | 28 | public DataReply(DataReplyStatus status, string code, string text) 29 | { 30 | Status = status; 31 | Messages = new [] { new DataReplyMessage { Code = code, Text = text } }; 32 | } 33 | 34 | public DataReply(DataReplyStatus status, DataReplyMessage message) 35 | { 36 | Status = status; 37 | if (message != null) 38 | Messages = new [] { message }; 39 | } 40 | 41 | public DataReply(DataReplyStatus status, DataReplyMessage[] messages) 42 | { 43 | Status = status; 44 | if (messages?.Length > 0) 45 | Messages = messages; 46 | } 47 | 48 | 49 | public DataReply(string message) 50 | { 51 | Status = DataReplyStatus.Ok; 52 | Messages = new [] { new DataReplyMessage { Text = message } }; 53 | } 54 | 55 | public static DataReplyStatus? ParseStatus (string statusCode) 56 | { 57 | if (IsNullOrWhiteSpace(statusCode)) 58 | return null; 59 | 60 | if (Enum.TryParse(statusCode, true, out DataReplyStatus status)) 61 | return status; 62 | 63 | throw new InvalidCastException( 64 | $"Cannot cast string '{statusCode}' to DataReplyStatus Enum. " + 65 | $"Available values: {Join(", ", Enum.GetNames(typeof(DataReplyStatus)))}"); 66 | } 67 | } 68 | 69 | 70 | [DataContract] 71 | public class DataReply: DataReply { 72 | 73 | [DataMember(EmitDefaultValue = false)] 74 | public TData Data { get; set; } 75 | 76 | public DataReply(TData data) 77 | { 78 | Data = data; 79 | } 80 | 81 | public DataReply() 82 | { 83 | Data = default; 84 | } 85 | 86 | public DataReply(DataReplyStatus status, string code, string text, TData data) :base(status, code, text) 87 | { 88 | Data = data; 89 | } 90 | 91 | public DataReply(DataReplyStatus status, TData data) :base(status) 92 | { 93 | Data = data; 94 | } 95 | 96 | public DataReply(DataReplyStatus status) :base(status) 97 | { 98 | Data = default; 99 | } 100 | 101 | public DataReply(DataReplyStatus status, string code, string text) :base(status, code, text) 102 | { 103 | Data = default; 104 | } 105 | 106 | public DataReply(DataReplyStatus status, DataReplyMessage replyMessage) :base(status, replyMessage) 107 | { 108 | Data = default; 109 | } 110 | 111 | public DataReply(DataReplyStatus status, DataReplyMessage[] replyMessages) :base(status, replyMessages) 112 | { 113 | Data = default; 114 | } 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /Artisan.Orm/DataReplyException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace Artisan.Orm 6 | { 7 | 8 | public class DataReplyException: Exception 9 | { 10 | public DataReplyStatus Status { get; } = DataReplyStatus.Error; 11 | 12 | public DataReplyMessage[] Messages { get; set; } 13 | 14 | public DataReplyException() {} 15 | 16 | public DataReplyException(DataReplyStatus status) 17 | { 18 | Status = status; 19 | } 20 | 21 | public DataReplyException(DataReplyStatus status, string code, long? id = null) 22 | { 23 | Status = status; 24 | Messages = new [] { new DataReplyMessage {Code = code, Id = id} }; 25 | } 26 | 27 | public DataReplyException(DataReplyStatus status, string code, long? id, object value) 28 | { 29 | Status = status; 30 | Messages = new [] { new DataReplyMessage {Code = code, Id = id, Value = value} }; 31 | } 32 | 33 | public DataReplyException(DataReplyStatus status, string code, string text, long? id = null) 34 | { 35 | Status = status; 36 | Messages = new [] { new DataReplyMessage {Code = code, Text = text, Id = id} }; 37 | } 38 | 39 | public DataReplyException(DataReplyStatus status, string code, string text, object value) 40 | { 41 | Status = status; 42 | Messages = new [] { new DataReplyMessage {Code = code, Text = text, Value = value} }; 43 | } 44 | 45 | public DataReplyException(DataReplyStatus status, string code, string text, long? id, object value) 46 | { 47 | Status = status; 48 | Messages = new [] { new DataReplyMessage {Code = code, Text = text, Id = id, Value = value} }; 49 | } 50 | 51 | public DataReplyException(DataReplyStatus status, DataReplyMessage replyMessage) 52 | { 53 | Status = status; 54 | Messages = new [] { replyMessage }; 55 | } 56 | 57 | public DataReplyException(DataReplyStatus status, DataReplyMessage[] messages) 58 | { 59 | Status = status; 60 | Messages = messages; 61 | } 62 | 63 | public override string Message 64 | { 65 | get 66 | { 67 | var sb = new StringBuilder(); 68 | 69 | foreach (var message in Messages) 70 | { 71 | if (sb.Length > 0) 72 | sb.Append(", "); 73 | 74 | sb.Append('{').Append($"Code: {message.Code}"); 75 | 76 | if (!String.IsNullOrWhiteSpace(message.Text)) 77 | sb.Append($", Text: {message.Text}"); 78 | 79 | if (message.Id.HasValue) 80 | sb.Append($", Id: {message.Id}"); 81 | 82 | if (message.Value != null) 83 | sb.Append($", Value: {message.Value}"); 84 | 85 | sb.Append('}'); 86 | } 87 | 88 | return sb.ToString(); 89 | } 90 | } 91 | 92 | public DataReplyMessage GetDataReplyMessage(string code) 93 | { 94 | return Messages?.FirstOrDefault(m => m.Code == code); 95 | } 96 | 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Artisan.Orm/DataReplyMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using Microsoft.Data.SqlClient; 4 | 5 | namespace Artisan.Orm 6 | { 7 | 8 | [DataContract] 9 | public class DataReplyMessage 10 | { 11 | [DataMember] 12 | public String Code; 13 | 14 | [DataMember(EmitDefaultValue = false)] 15 | public String Text; 16 | 17 | [DataMember(EmitDefaultValue = false)] 18 | public Int64? Id; 19 | 20 | [DataMember(EmitDefaultValue = false)] 21 | public Object Value; 22 | } 23 | 24 | [MapperFor( typeof(DataReplyMessage), RequiredMethod.CreateObject)] 25 | public static class DataReplyMessageMapper 26 | { 27 | public static DataReplyMessage CreateObject(SqlDataReader dr) 28 | { 29 | var i = 0; 30 | 31 | return new DataReplyMessage 32 | { 33 | Code = dr.GetString (i) , 34 | Text = dr.GetStringNullable (++i) , 35 | Id = dr.GetValueNullable (++i) , 36 | Value = dr.GetValueNullable (++i) 37 | }; 38 | } 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Artisan.Orm/DataReplyState.cs: -------------------------------------------------------------------------------- 1 | namespace Artisan.Orm 2 | { 3 | 4 | public enum DataReplyStatus 5 | { 6 | Ok , 7 | Fail , 8 | Missing , 9 | Validation , 10 | Concurrency , 11 | Denial , 12 | Error 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Artisan.Orm/DataTableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using static System.String; 8 | 9 | namespace Artisan.Orm 10 | { 11 | public static class DataTableExtensions 12 | { 13 | public static DataTable AddColumn(this DataTable dataTable, string columnName, bool isNullable = true) 14 | { 15 | var underlyingType = typeof(T).GetUnderlyingType(); 16 | var column = new DataColumn(columnName, underlyingType) {AllowDBNull = isNullable}; 17 | dataTable.Columns.Add(column); 18 | return dataTable; 19 | } 20 | 21 | public static DataTable ToDataTable(this IEnumerable list) 22 | { 23 | if (!MappingManager.GetCreateDataFuncs(out Func createDataTableFunc, out Func createDataRowFunc)) 24 | throw new KeyNotFoundException($"No mapping function found to create DataTable and DataRow for type {typeof(T).FullName}"); 25 | 26 | var dataTable = createDataTableFunc(); 27 | 28 | foreach (var entity in list.Where(entity => entity != null)) 29 | { 30 | dataTable.Rows.Add(createDataRowFunc(entity)); 31 | } 32 | 33 | return dataTable; 34 | } 35 | 36 | public static DataTable ToDataTable(this IEnumerable list, Func getDataTableFunc, Func convertToDataRowFunc) 37 | { 38 | var dataTable = getDataTableFunc(); 39 | 40 | foreach (var entity in list.Where(entity => entity != null)) 41 | { 42 | dataTable.Rows.Add(convertToDataRowFunc(entity)); 43 | } 44 | 45 | return dataTable; 46 | } 47 | 48 | public static DataTable ToTinyIntIdDataTable(this IEnumerable ids, string dataTableName = "TinyIntIdTableType", string columnName = "Id" ) 49 | { 50 | return ids.ToIdDataTable(dataTableName, columnName); 51 | } 52 | 53 | public static DataTable ToSmallIntIdDataTable(this IEnumerable ids, string dataTableName = "SmallIntIdTableType", string columnName = "Id" ) 54 | { 55 | return ids.ToIdDataTable(dataTableName, columnName); 56 | } 57 | 58 | public static DataTable ToIntIdDataTable(this IEnumerable ids, string dataTableName = "IntIdTableType", string columnName = "Id") 59 | { 60 | return ids.ToIdDataTable(dataTableName, columnName); 61 | } 62 | 63 | public static DataTable ToBigIntIdDataTable(this IEnumerable ids, string dataTableName = "BigIntIdTableType", string columnName = "Id") 64 | { 65 | return ids.ToIdDataTable(dataTableName, columnName); 66 | } 67 | 68 | private static DataTable ToIdDataTable(this IEnumerable ids, string dataTableName, string columnName) where T: struct 69 | { 70 | var dataTable = new DataTable(dataTableName); 71 | dataTable.Columns.Add(columnName, typeof(T)); 72 | 73 | if (ids != null) 74 | foreach (var id in ids) 75 | dataTable.Rows.Add(id); 76 | 77 | return dataTable; 78 | } 79 | 80 | 81 | public static DataTable AsDataTable(this IEnumerable list, string tableName = null, string columnNames = null) 82 | { 83 | var columnNameArray = columnNames?.Split(',', ';').Select(s => s.Trim()).ToArray(); 84 | return list.AsDataTable(tableName, columnNameArray); 85 | } 86 | 87 | public static DataTable AsDataTable(this IEnumerable list, string tableName, string[] columnNameArray) 88 | { 89 | if (typeof(T).IsSimpleType()) 90 | return GetSimpleTypeDataTable(list, tableName, columnNameArray); 91 | 92 | return GetObjectDataTable(list, tableName, columnNameArray); 93 | } 94 | 95 | private static DataTable GetSimpleTypeDataTable(IEnumerable list, string tableName, string[] columnNameArray) 96 | { 97 | var underlyingType = typeof(T).GetUnderlyingType(); 98 | 99 | var dataTable = new DataTable(tableName); 100 | 101 | var columnName = columnNameArray?.Length > 0 ? columnNameArray[0] : null; 102 | 103 | if (IsNullOrWhiteSpace(columnName)) 104 | columnName = underlyingType.Name; 105 | 106 | dataTable.Columns.Add(columnName, underlyingType); 107 | 108 | if (list != null) 109 | foreach (var value in list) 110 | dataTable.Rows.Add(value); 111 | 112 | return dataTable; 113 | } 114 | 115 | private static DataTable GetObjectDataTable(IEnumerable list, string tableName, string[] columnNameArray) 116 | { 117 | string key = GetAutoCreateDataFuncsKey(tableName, columnNameArray); 118 | 119 | if (!MappingManager.GetAutoCreateDataFuncs(key, out Func createDataTableFunc, out Func createDataRowFunc)) 120 | { 121 | CreateAutoMappingFunc(tableName, columnNameArray, out createDataTableFunc, out createDataRowFunc); 122 | 123 | MappingManager.AddAutoCreateDataFuncs(key, createDataTableFunc, createDataRowFunc); 124 | } 125 | 126 | return list.ToDataTable(createDataTableFunc, createDataRowFunc); 127 | } 128 | 129 | private static string GetAutoCreateDataFuncsKey(string tableName, string[] columnNameArray) 130 | { 131 | var typeFullName = typeof(T).FullName; 132 | 133 | var tableNamePart = !IsNullOrWhiteSpace(tableName) ? $"+{tableName}" : ""; 134 | 135 | var columnNames = columnNameArray?.Length > 0 ? $"+{Join("+", columnNameArray)}" : ""; 136 | 137 | return $"{typeFullName}{tableNamePart}{columnNames}"; 138 | } 139 | 140 | private static void CreateAutoMappingFunc(string tableName, string[] columnNameArray, out Func createDataTableFunc, out Func createDataRowFunc) 141 | { 142 | var bindingFlags = BindingFlags.Public | BindingFlags.Instance ; 143 | var properties = typeof(T).GetProperties(bindingFlags).Where(p => p.PropertyType.IsSimpleType() && p.CanRead).ToList(); 144 | 145 | if (columnNameArray?.Length > 0) 146 | { 147 | var sortedProperties = new List(); 148 | 149 | foreach (var columnName in columnNameArray) 150 | { 151 | var property = properties.FirstOrDefault(p => p.Name == columnName); 152 | 153 | if (property != null) 154 | sortedProperties.Add(property); 155 | } 156 | 157 | properties = sortedProperties; 158 | } 159 | 160 | createDataTableFunc = GetCreateDataTableFunc(tableName, properties); 161 | 162 | createDataRowFunc = GetCreateDataRowFunc(properties); 163 | } 164 | 165 | private static Func GetCreateDataTableFunc(string tableName, List properties) 166 | { 167 | var dataTableCtor = Expression.New(typeof (DataTable)); 168 | var tableNameProp = typeof (DataTable).GetProperty("TableName"); 169 | var tableNameConst = Expression.Constant(tableName, typeof (string)); 170 | var tableNameBinding = Expression.Bind(tableNameProp, tableNameConst); 171 | var dataTableInit = Expression.MemberInit(dataTableCtor, tableNameBinding); 172 | 173 | var dataTable = Expression.Variable(typeof (DataTable), "dataTable"); 174 | var columns = Expression.PropertyOrField(dataTable, "Columns"); 175 | 176 | List addColumnCalls = new List(); 177 | 178 | foreach (var property in properties) 179 | { 180 | var columnCtor = Expression.New(typeof (DataColumn)); 181 | 182 | var columnNameProp = typeof (DataColumn).GetProperty("ColumnName"); 183 | var columnNameConst = Expression.Constant(property.Name, typeof (string)); 184 | var columnNameBinding = Expression.Bind(columnNameProp, columnNameConst); 185 | 186 | var columnTypeProp = typeof (DataColumn).GetProperty("DataType"); 187 | var columnTypeConst = Expression.Constant(property.PropertyType.GetUnderlyingType(), typeof (Type)); 188 | var columnTypeBinding = Expression.Bind(columnTypeProp, columnTypeConst); 189 | 190 | var columnBindings = new List {columnNameBinding, columnTypeBinding}; 191 | 192 | var columnInit = Expression.MemberInit(columnCtor, columnBindings); 193 | 194 | addColumnCalls.Add( 195 | Expression.Call( 196 | columns, 197 | typeof (DataColumnCollection).GetMethod("Add", new[] {typeof (DataColumn)}), 198 | columnInit 199 | ) 200 | ); 201 | } 202 | 203 | BlockExpression addColumnsBlock = Expression.Block(addColumnCalls); 204 | 205 | var body = Expression.Block( 206 | 207 | // DataTable dataTable; 208 | new[] {dataTable}, 209 | 210 | // dataTable = new DataTable("TableName"); 211 | Expression.Assign(dataTable, dataTableInit), 212 | 213 | // foreach - dataTable.Columns.Add("ColumnName", ColumnType) 214 | addColumnsBlock, 215 | 216 | // return dataTable 217 | dataTable 218 | ); 219 | 220 | var createDataTableFunc = Expression.Lambda>(body).Compile(); 221 | 222 | return createDataTableFunc; 223 | } 224 | 225 | private static Func GetCreateDataRowFunc(List properties) 226 | { 227 | var objParam = Expression.Parameter( typeof (T), "obj"); 228 | 229 | var items = new List(); 230 | 231 | foreach (var property in properties) 232 | { 233 | var getterMethodInfo = property.GetGetMethod(); 234 | var getterCall = Expression.Call(objParam, getterMethodInfo); 235 | var castToObject = Expression.Convert(getterCall, typeof(object)); 236 | 237 | items.Add(castToObject); 238 | } 239 | 240 | var objectArrayExpression = Expression.NewArrayInit(typeof(object), items); 241 | 242 | var createDataRowFunc = Expression.Lambda>(objectArrayExpression, objParam).Compile(); 243 | 244 | return createDataRowFunc; 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Artisan.Orm/INode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Artisan.Orm 4 | { 5 | 6 | public interface INode where T: class 7 | { 8 | int Id { get; set; } 9 | 10 | int? ParentId { get; set; } 11 | 12 | IList Children { get; set; } 13 | } 14 | 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Artisan.Orm/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2025 Vadim Loboda 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 | -------------------------------------------------------------------------------- /Artisan.Orm/Logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lobodava/artisan-orm/77db37ce13bd1c3bf2f08047de5f418363d7f353/Artisan.Orm/Logo.ico -------------------------------------------------------------------------------- /Artisan.Orm/MapperForAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Artisan.Orm 4 | { 5 | 6 | public enum RequiredMethod 7 | { 8 | /// 9 | /// Check if all four methods exists: CreateObject, CreateObjectRow, CreateDataTable and CreateDataRow. 10 | /// 11 | All, 12 | 13 | /// 14 | /// Check if all three main methods exists: CreateObject, CreateObjectRow and CreateDataTable. 15 | /// 16 | AllMain, 17 | 18 | /// 19 | /// Check if both CreateObject and CreateObjectRow methods exist. 20 | /// 21 | BothForObject, 22 | 23 | /// 24 | /// Check if CreateDataTable and CreateDataRow methods exist. 25 | /// 26 | BothForDataTable, 27 | 28 | /// 29 | /// Check if CreateObject method exists. 30 | /// 31 | CreateObject, 32 | 33 | /// 34 | /// Check if CreateObjectRow method exists. 35 | /// 36 | CreateObjectRow, 37 | 38 | //CreateDataTable, 39 | //CreateDataRow, 40 | } 41 | 42 | 43 | [AttributeUsage(AttributeTargets.Class)] // , AllowMultiple = true 44 | public class MapperForAttribute: Attribute 45 | { 46 | public Type MapperForType { get; } 47 | 48 | public RequiredMethod[] RequiredMethods { get; } 49 | 50 | public MapperForAttribute(Type mapperForType) { 51 | MapperForType = mapperForType; 52 | RequiredMethods = Array.Empty(); 53 | } 54 | 55 | public MapperForAttribute(Type mapperForType, params RequiredMethod[] requiredMethods) { 56 | MapperForType = mapperForType; 57 | RequiredMethods = requiredMethods; 58 | } 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /Artisan.Orm/MappingManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Reflection; 7 | using Microsoft.Data.SqlClient; 8 | 9 | namespace Artisan.Orm 10 | { 11 | 12 | public static class MappingManager 13 | { 14 | private static readonly Dictionary CreateObjectFuncDictionary = new Dictionary(); 15 | 16 | private static readonly Dictionary CreateObjectRowFuncDictionary = new Dictionary(); 17 | 18 | private static readonly Dictionary, Delegate>> CreateDataFuncsDictionary = new Dictionary, Delegate>>(); 19 | 20 | private static readonly ConcurrentDictionary AutoCreateObjectFuncDictionary = new ConcurrentDictionary(); 21 | 22 | private static readonly ConcurrentDictionary, Delegate>> AutoCreateDataFuncsDictionary = new ConcurrentDictionary, Delegate>>(); 23 | 24 | private static readonly ConcurrentDictionary SqlParametersDictionary = new ConcurrentDictionary(); 25 | 26 | 27 | static MappingManager() 28 | { 29 | foreach (var type in GetTypesWithMapperForAttribute()) 30 | { 31 | var attributes = type.GetCustomAttributes(typeof(MapperForAttribute), true); 32 | 33 | if (attributes.Length == 0) continue; 34 | 35 | foreach (var attribute in attributes.Cast()) 36 | { 37 | 38 | var methodInfo = type.GetMethod("CreateObject", new Type[] { typeof(SqlDataReader) }); 39 | 40 | if (methodInfo == null) 41 | { 42 | if (attribute.RequiredMethods.Intersect(new []{RequiredMethod.All, RequiredMethod.AllMain, RequiredMethod.BothForObject, RequiredMethod.CreateObject}).Any()) 43 | throw new NullReferenceException($"Mapper {type.Name} does not contain required method CreateObject"); 44 | } 45 | else 46 | { 47 | var funcType = typeof(Func<,>).MakeGenericType(typeof(SqlDataReader), attribute.MapperForType); 48 | var del = Delegate.CreateDelegate(funcType, methodInfo); 49 | 50 | CreateObjectFuncDictionary.Add(attribute.MapperForType, del); 51 | } 52 | 53 | 54 | methodInfo = type.GetMethod("CreateObjectRow", new Type[] { typeof(SqlDataReader) }); 55 | 56 | if (methodInfo == null) 57 | { 58 | if (attribute.RequiredMethods.Intersect(new []{RequiredMethod.All, RequiredMethod.BothForObject, RequiredMethod.CreateObjectRow}).Any()) 59 | throw new NullReferenceException($"Mapper {type.Name} does not contain required method CreateObjectRow"); 60 | } 61 | else 62 | { 63 | var funcType = typeof(Func<,>).MakeGenericType(typeof(SqlDataReader), typeof(ObjectRow)); 64 | var createObjectRowDelegate = Delegate.CreateDelegate(funcType, methodInfo); 65 | 66 | CreateObjectRowFuncDictionary.Add(attribute.MapperForType, createObjectRowDelegate); 67 | } 68 | 69 | Func createDataTableFunc = null; 70 | 71 | methodInfo = type.GetMethod("CreateDataTable"); 72 | 73 | if (methodInfo == null) 74 | { 75 | if (attribute.RequiredMethods.Intersect(new []{RequiredMethod.All, RequiredMethod.BothForDataTable }).Any()) 76 | throw new NullReferenceException($"Mapper {type.Name} does not contain required method CreateDataTable"); 77 | } 78 | else 79 | { 80 | createDataTableFunc = (Func)Delegate.CreateDelegate(typeof(Func), methodInfo); 81 | } 82 | 83 | 84 | Delegate createDataRowDelegate = null; 85 | 86 | 87 | methodInfo = type.GetMethod("CreateDataRow", new Type[] { attribute.MapperForType }); 88 | 89 | if (methodInfo == null) { 90 | if (attribute.RequiredMethods.Intersect(new []{RequiredMethod.All, RequiredMethod.BothForDataTable }).Any()) 91 | throw new NullReferenceException($"Mapper {type.Name} does not contain required method CreateDataRow"); 92 | } 93 | else { 94 | var funcType = typeof(Func<,>).MakeGenericType(attribute.MapperForType, typeof(object[])); 95 | createDataRowDelegate = Delegate.CreateDelegate(funcType, methodInfo); 96 | } 97 | 98 | CreateDataFuncsDictionary.Add(attribute.MapperForType, Tuple.Create(createDataTableFunc, createDataRowDelegate)); 99 | 100 | } 101 | } 102 | } 103 | 104 | 105 | public static Func GetCreateObjectFunc() 106 | { 107 | if (CreateObjectFuncDictionary.TryGetValue(typeof(T), out Delegate del)) 108 | return (Func)del; 109 | 110 | throw new NullReferenceException($"CreateObject Func not found. Check if MapperFor {typeof(T).FullName} exists and CreateObject exist."); 111 | } 112 | 113 | public static Func GetCreateObjectRowFunc() 114 | { 115 | if (CreateObjectRowFuncDictionary.TryGetValue(typeof(T), out Delegate del)) 116 | return (Func)del; 117 | 118 | throw new NullReferenceException($"CreateRow Func not found. Check if MapperFor {typeof(T).FullName} and CreateRow exist."); 119 | } 120 | 121 | 122 | public static Func GetCreateDataTableFunc() 123 | { 124 | return CreateDataFuncsDictionary.TryGetValue(typeof(T), out Tuple, Delegate> tuple) 125 | ? tuple.Item1 126 | : null; 127 | } 128 | 129 | public static Func GetCreateDataRowFunc() 130 | { 131 | return CreateDataFuncsDictionary.TryGetValue(typeof(T), out Tuple, Delegate> tuple) 132 | ? (Func)tuple.Item2 133 | : null; 134 | } 135 | 136 | 137 | public static bool GetCreateDataFuncs(out Func createDataTableFunc, out Func createDataRowFunc) 138 | { 139 | if (CreateDataFuncsDictionary.TryGetValue(typeof(T), out Tuple, Delegate> tuple)) 140 | { 141 | createDataTableFunc = tuple.Item1; 142 | createDataRowFunc = (Func)tuple.Item2; 143 | 144 | return true; 145 | } 146 | 147 | createDataTableFunc = null; 148 | createDataRowFunc = null; 149 | 150 | return false; 151 | } 152 | 153 | 154 | public static bool GetCreateDataFuncs(Type type, out Func createDataTableFunc, out Delegate createDataRowFunc) 155 | { 156 | if (CreateDataFuncsDictionary.TryGetValue(type, out Tuple, Delegate> funcs)) 157 | { 158 | createDataTableFunc = funcs.Item1; 159 | createDataRowFunc = funcs.Item2; 160 | 161 | return true; 162 | } 163 | 164 | createDataTableFunc = null; 165 | createDataRowFunc = null; 166 | 167 | return false; 168 | } 169 | 170 | public static bool AddAutoCreateObjectFunc(string key, Func autoCreateObjectFunc) 171 | { 172 | return AutoCreateObjectFuncDictionary.TryAdd(key, autoCreateObjectFunc); 173 | } 174 | 175 | public static Func GetAutoCreateObjectFunc(string key) 176 | { 177 | if (AutoCreateObjectFuncDictionary.TryGetValue(key, out Delegate del)) 178 | { 179 | return (Func)del; 180 | } 181 | return null; 182 | } 183 | 184 | public static bool AddAutoCreateDataFuncs(string key, Func createDataTableFunc, Func createDataRowFunc) 185 | { 186 | var funcs = new Tuple, Delegate>(createDataTableFunc, createDataRowFunc); 187 | 188 | return AutoCreateDataFuncsDictionary.TryAdd(key, funcs); 189 | } 190 | 191 | public static bool GetAutoCreateDataFuncs(string key, out Func createDataTableFunc, out Func createDataRowFunc) 192 | { 193 | if (AutoCreateDataFuncsDictionary.TryGetValue(key, out Tuple, Delegate> funcs)) 194 | { 195 | createDataTableFunc = funcs.Item1; 196 | createDataRowFunc = (Func)funcs.Item2; 197 | 198 | return true; 199 | } 200 | 201 | createDataTableFunc = null; 202 | createDataRowFunc = null; 203 | 204 | return false; 205 | } 206 | 207 | private static IEnumerable GetTypesWithMapperForAttribute() 208 | { 209 | foreach (Assembly assembly in GetCurrentAndDependentAssemblies()) 210 | { 211 | foreach (Type type in assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(MapperForAttribute), true).Length > 0)) 212 | { 213 | yield return type; 214 | } 215 | } 216 | } 217 | 218 | public static bool AddSqlParameters(string key, SqlParameter[] sqlParameters) 219 | { 220 | return SqlParametersDictionary.TryAdd(key, sqlParameters); 221 | } 222 | 223 | public static SqlParameter[] GetSqlParameters(string key) 224 | { 225 | return SqlParametersDictionary.TryGetValue(key, out var collection) ? collection : null; 226 | } 227 | 228 | #region [ Get Dependent Assemblies ] 229 | 230 | private static IEnumerable GetCurrentAndDependentAssemblies() 231 | { 232 | var currentAssembly = typeof(MappingManager).Assembly; 233 | 234 | return AppDomain.CurrentDomain.GetAssemblies() 235 | 236 | // http://stackoverflow.com/a/8850495/623190 237 | .Where(a => GetNamesOfAssembliesReferencedBy(a).Contains(currentAssembly.FullName)) 238 | .Concat(new[] { currentAssembly }) 239 | 240 | // https://www.codeproject.com/Articles/1155836/Artisan-Orm-or-How-To-Reinvent-the-Wheel?msg=5419092#xx5419092xx 241 | .GroupBy(a => a.FullName) 242 | .Select(x => x.First()); 243 | } 244 | 245 | public static IEnumerable GetNamesOfAssembliesReferencedBy(Assembly assembly) 246 | { 247 | return assembly.GetReferencedAssemblies() 248 | .Select(assemblyName => assemblyName.FullName); 249 | } 250 | 251 | #endregion 252 | 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /Artisan.Orm/NegativeIdentity.cs: -------------------------------------------------------------------------------- 1 | namespace Artisan.Orm 2 | { 3 | 4 | public static class Int64NegativeIdentity 5 | { 6 | private static long _identity = long.MinValue; 7 | 8 | public static long Next => _identity++; 9 | } 10 | 11 | public static class Int32NegativeIdentity 12 | { 13 | private static int _identity = int.MinValue; 14 | 15 | public static int Next => _identity++; 16 | } 17 | 18 | public static class Int16NegativeIdentity 19 | { 20 | private static short _identity = short.MinValue; 21 | 22 | public static short Next => _identity++; 23 | } 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Artisan.Orm/ObjectRows.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Artisan.Orm 4 | { 5 | 6 | public class ObjectRow: List 7 | { 8 | public ObjectRow() : base(new List()) 9 | { 10 | } 11 | 12 | public ObjectRow(int capacity) : base(new List(capacity)) 13 | { 14 | } 15 | } 16 | 17 | 18 | public class ObjectRows: List 19 | { 20 | public ObjectRows() : base(new List()) 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Artisan.Orm/SqlDataReaderExtensions_CreateObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using Microsoft.Data.SqlClient; 8 | 9 | namespace Artisan.Orm 10 | { 11 | 12 | public static partial class SqlDataReaderExtensions 13 | { 14 | 15 | #region [ CreateObject ] 16 | 17 | public static T CreateObject(this SqlDataReader dr) 18 | { 19 | var key = GetAutoCreateObjectFuncKey(dr); 20 | 21 | var autoMappingFunc = MappingManager.GetAutoCreateObjectFunc(key); 22 | 23 | return CreateObject(dr, autoMappingFunc, key); 24 | } 25 | 26 | internal static T CreateObject(this SqlDataReader dr, Func autoMappingFunc, string key) 27 | { 28 | if (autoMappingFunc == null) 29 | { 30 | autoMappingFunc = CreateAutoMappingFunc(dr); 31 | MappingManager.AddAutoCreateObjectFunc(key, autoMappingFunc); 32 | } 33 | 34 | return autoMappingFunc(dr); 35 | } 36 | 37 | public static dynamic CreateDynamic(this SqlDataReader dr) 38 | { 39 | dynamic expando = new ExpandoObject(); 40 | var dict = expando as IDictionary; 41 | 42 | for (var i = 0; i < dr.FieldCount; i++) 43 | { 44 | var columnName = dr.GetName(i); 45 | var value = dr.GetValue(i); 46 | dict.Add(columnName, value == DBNull.Value ? null : value); 47 | } 48 | 49 | return expando; 50 | } 51 | 52 | #endregion 53 | 54 | #region [ private members ] 55 | 56 | private static readonly PropertyInfo CommandProperty = typeof(SqlDataReader).GetProperties(BindingFlags.NonPublic | BindingFlags.Instance).First(p => p.Name == "Command"); 57 | 58 | internal static string GetCommandText(this SqlDataReader dr) 59 | { 60 | var command = (SqlCommand)CommandProperty.GetValue(dr); 61 | return command.CommandText; 62 | } 63 | 64 | internal static string GetAutoCreateObjectFuncKey(SqlDataReader dr) 65 | { 66 | return GetAutoCreateObjectFuncKey(dr.GetCommandText()); 67 | } 68 | 69 | internal static string GetAutoCreateObjectFuncKey(string commandText) 70 | { 71 | return $"{commandText}+{typeof(T).FullName}"; 72 | } 73 | 74 | private static readonly Dictionary ReaderGetMethodNames = new Dictionary() 75 | { 76 | { typeof(Boolean) , "GetBoolean" }, 77 | { typeof(Byte) , "GetByte" }, 78 | { typeof(Int16) , "GetInt16" }, 79 | { typeof(Int32) , "GetInt32" }, 80 | { typeof(Int64) , "GetInt64" }, 81 | { typeof(Single) , "GetFloat" }, 82 | { typeof(Double) , "GetDouble" }, 83 | { typeof(Decimal) , "GetDecimal" }, 84 | { typeof(String) , "GetString" }, 85 | { typeof(DateTime) , "GetDateTime" }, 86 | { typeof(DateTimeOffset) , "GetDateTimeOffset" }, 87 | { typeof(TimeSpan) , "GetTimeSpan" }, 88 | { typeof(Guid) , "GetGuid" } 89 | }; 90 | 91 | private static MethodCallExpression GetTypedValueMethodCallExpression(Type propertyType, Type fieldType, ParameterExpression sqlDataReaderParam, ConstantExpression indexConst, out bool isDefaultGetValueMethod) 92 | { 93 | var underlyingType = propertyType.GetUnderlyingType(); 94 | 95 | isDefaultGetValueMethod = false; 96 | 97 | if (propertyType == fieldType) 98 | { 99 | if (ReaderGetMethodNames.TryGetValue(underlyingType, out string methodName)) 100 | return Expression.Call( 101 | sqlDataReaderParam, 102 | typeof(SqlDataReader).GetMethod(methodName, new[] { typeof(int) }), 103 | indexConst 104 | ); 105 | 106 | if (underlyingType == typeof(Char)) 107 | return Expression.Call( 108 | null, 109 | typeof(SqlDataReaderExtensions).GetMethod("GetCharacter", new[] { typeof(SqlDataReader), typeof(int) }), 110 | sqlDataReaderParam, 111 | indexConst 112 | ); 113 | } 114 | 115 | isDefaultGetValueMethod = true; 116 | 117 | return Expression.Call( 118 | null, 119 | typeof(Convert).GetMethod("ChangeType", new[] { typeof(object), typeof(Type) }), 120 | Expression.Call( 121 | sqlDataReaderParam, 122 | typeof(SqlDataReader).GetMethod("GetValue", new[] { typeof(int) }), 123 | indexConst 124 | ), 125 | Expression.Constant(underlyingType, typeof(Type)) 126 | ); 127 | } 128 | 129 | internal static Func CreateAutoMappingFunc(SqlDataReader dr) 130 | { 131 | var properties = typeof(T) 132 | .GetProperties(BindingFlags.Public | BindingFlags.Instance) 133 | .Where(p => p.CanWrite && p.PropertyType.IsSimpleType()).ToList(); 134 | 135 | var memberBindings = new List(); 136 | var readerParam = Expression.Parameter(typeof(SqlDataReader), "reader"); 137 | 138 | for (var i = 0; i < dr.FieldCount; i++) 139 | { 140 | var columnName = dr.GetName(i); 141 | var prop = properties.FirstOrDefault(p => p.Name == columnName); 142 | 143 | if (prop != null) 144 | { 145 | var indexConst = Expression.Constant(i, typeof(int)); 146 | var fieldType = dr.GetFieldType(i); 147 | 148 | MethodCallExpression getTypedValueExp = GetTypedValueMethodCallExpression(prop.PropertyType, fieldType, readerParam, indexConst, out bool isDefaultGetValueMethod); 149 | 150 | Expression getValueExp = null; 151 | 152 | if (prop.PropertyType.IsNullableValueType()) 153 | { 154 | getValueExp = Expression.Condition ( 155 | Expression.Call( 156 | readerParam, 157 | typeof(SqlDataReader).GetMethod("IsDBNull", new[] { typeof(int) }), 158 | indexConst 159 | ), 160 | 161 | Expression.Default(prop.PropertyType), 162 | 163 | Expression.Convert(getTypedValueExp, prop.PropertyType) 164 | ); 165 | } 166 | else if (isDefaultGetValueMethod) 167 | { 168 | getValueExp = Expression.Convert(getTypedValueExp, prop.PropertyType); 169 | } 170 | else 171 | { 172 | getValueExp = getTypedValueExp; 173 | } 174 | 175 | var binding = Expression.Bind(prop, getValueExp); 176 | memberBindings.Add(binding); 177 | } 178 | } 179 | 180 | if (memberBindings.Count == 0) 181 | throw new ApplicationException($"Creation of AutoMapping Func failed. No property-field name matching found for class = '{typeof(T).FullName}' and CommandText = '{dr.GetCommandText()}'"); 182 | 183 | 184 | var ctor = Expression.New(typeof(T)); 185 | var memberInit = Expression.MemberInit(ctor, memberBindings); 186 | 187 | var func = Expression.Lambda>(memberInit, readerParam).Compile(); 188 | 189 | 190 | return func; 191 | } 192 | 193 | #endregion 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /Artisan.Orm/SqlDataReaderExtensions_Get.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.SqlTypes; 3 | using System.Globalization; 4 | using System.Linq; 5 | using Microsoft.Data.SqlClient; 6 | 7 | namespace Artisan.Orm 8 | { 9 | 10 | public static partial class SqlDataReaderExtensions 11 | { 12 | internal static T GetValue(this SqlDataReader reader) 13 | { 14 | return reader.GetValue(0); 15 | } 16 | 17 | public static T GetValue(this SqlDataReader reader, int ordinal) 18 | { 19 | var underlyingType = typeof(T).GetUnderlyingType(); 20 | 21 | return (T)Convert.ChangeType(reader.GetValue(ordinal), underlyingType); 22 | } 23 | 24 | internal static T GetValue(SqlDataReader reader, Type underlyingType) 25 | { 26 | return (T)Convert.ChangeType(reader.GetValue(0), underlyingType); 27 | } 28 | 29 | public static T GetValueNullable(this SqlDataReader reader, int ordinal) 30 | { 31 | if (reader.IsDBNull(ordinal)) 32 | return default; 33 | 34 | var underlyingType = typeof(T).GetUnderlyingType(); 35 | 36 | return (T)Convert.ChangeType(reader.GetValue(ordinal), underlyingType); 37 | } 38 | 39 | 40 | public static bool? GetBooleanNullable(this SqlDataReader reader, int ordinal) 41 | { 42 | return reader.IsDBNull(ordinal) ? default(bool?) : reader.GetBoolean(ordinal); 43 | } 44 | 45 | public static bool GetBoolean(this SqlDataReader reader, int ordinal, bool defaultValue) 46 | { 47 | return reader.IsDBNull(ordinal) ? defaultValue : reader.GetBoolean(ordinal); 48 | } 49 | 50 | public static byte? GetByteNullable(this SqlDataReader reader, int ordinal) 51 | { 52 | return reader.IsDBNull(ordinal) ? default(byte?) : reader.GetByte(ordinal); 53 | } 54 | 55 | public static short? GetInt16Nullable(this SqlDataReader reader, int ordinal) 56 | { 57 | return reader.IsDBNull(ordinal) ? default(short?) : reader.GetInt16(ordinal); 58 | } 59 | 60 | public static int? GetInt32Nullable(this SqlDataReader reader, int ordinal) 61 | { 62 | return reader.IsDBNull(ordinal) ? default(int?) : reader.GetInt32(ordinal); 63 | } 64 | 65 | public static long? GetInt64Nullable(this SqlDataReader reader, int ordinal) 66 | { 67 | return reader.IsDBNull(ordinal) ? default(long?) : reader.GetInt64(ordinal); 68 | } 69 | 70 | public static float? GetFloatNullable(this SqlDataReader reader, int ordinal) 71 | { 72 | return reader.IsDBNull(ordinal) ? default(float?) : reader.GetFloat(ordinal); 73 | } 74 | 75 | public static double? GetDoubleNullable(this SqlDataReader reader, int ordinal) 76 | { 77 | return reader.IsDBNull(ordinal) ? default(double?) : reader.GetDouble(ordinal); 78 | } 79 | 80 | public static decimal? GetDecimalNullable(this SqlDataReader reader, int ordinal) 81 | { 82 | return reader.IsDBNull(ordinal) ? default(decimal?) : reader.GetDecimal(ordinal); 83 | } 84 | 85 | public static decimal GetBigDecimal(this SqlDataReader reader, int ordinal) 86 | { 87 | return decimal.Parse(reader.GetSqlDecimal(ordinal).ToString(), CultureInfo.InvariantCulture); 88 | } 89 | 90 | public static decimal? GetBigDecimalNullable(this SqlDataReader reader, int ordinal) 91 | { 92 | return reader.IsDBNull(ordinal) ? default(decimal?) : reader.GetBigDecimal(ordinal); 93 | } 94 | 95 | public static char GetCharacter(this SqlDataReader reader, int ordinal) 96 | { 97 | var buffer = new char[1]; 98 | reader.GetChars(ordinal, 0, buffer, 0, 1); 99 | return buffer[0]; 100 | } 101 | 102 | public static char? GetCharacterNullable(this SqlDataReader reader, int ordinal) 103 | { 104 | if (reader.IsDBNull(ordinal)) 105 | return null; 106 | 107 | return reader.GetCharacter(ordinal); 108 | } 109 | 110 | public static string GetStringNullable(this SqlDataReader reader, int ordinal) 111 | { 112 | return reader.IsDBNull(ordinal) ? null : reader.GetString(ordinal); 113 | } 114 | 115 | public static DateTime? GetDateTimeNullable(this SqlDataReader reader, int ordinal) 116 | { 117 | return reader.IsDBNull(ordinal) ? default(DateTime?) : reader.GetDateTime(ordinal); 118 | } 119 | 120 | public static DateTimeOffset? GetDateTimeOffsetNullable(this SqlDataReader reader, int ordinal) 121 | { 122 | return reader.IsDBNull(ordinal) ? default(DateTimeOffset?) : reader.GetDateTimeOffset(ordinal); 123 | } 124 | 125 | public static DateTime GetUtcDateTime(this SqlDataReader reader, int ordinal) 126 | { 127 | return DateTime.SpecifyKind(reader.GetDateTime(ordinal), DateTimeKind.Utc); 128 | } 129 | 130 | public static TimeSpan? GetTimeSpanNullable(this SqlDataReader reader, int ordinal) 131 | { 132 | return reader.IsDBNull(ordinal) ? default(TimeSpan?) : reader.GetTimeSpan(ordinal); 133 | } 134 | 135 | public static Guid GetGuidFromString(this SqlDataReader reader, int ordinal) 136 | { 137 | return reader.IsDBNull(ordinal) ? Guid.Empty : Guid.Parse(reader.GetString(ordinal)); 138 | } 139 | 140 | public static Guid? GetGuidFromStringNullable(this SqlDataReader reader, int ordinal) 141 | { 142 | return reader.IsDBNull(ordinal) ? default(Guid?) : Guid.Parse(reader.GetString(ordinal)); 143 | } 144 | 145 | public static Guid GetGuidFromString(this SqlDataReader reader, int ordinal, Guid defaultValue) 146 | { 147 | return reader.IsDBNull(ordinal) ? defaultValue : Guid.Parse(reader.GetString(ordinal)); 148 | } 149 | 150 | public static Guid? GetGuidNullable(this SqlDataReader reader, int ordinal) 151 | { 152 | return reader.IsDBNull(ordinal) ? default(Guid?) : reader.GetGuid(ordinal); 153 | } 154 | 155 | public static Guid GetGuid(this SqlDataReader reader, int ordinal, Guid defaultValue) 156 | { 157 | return reader.IsDBNull(ordinal) ? defaultValue : reader.GetGuid(ordinal); 158 | } 159 | 160 | public static byte[] GetBytesFromRowVersion(this SqlDataReader reader, int ordinal) 161 | { 162 | if (reader.IsDBNull(ordinal)) 163 | return null; 164 | 165 | return (byte[])reader.GetValue(ordinal); 166 | } 167 | 168 | public static long GetInt64FromRowVersion(this SqlDataReader reader, int ordinal) 169 | { 170 | return BitConverter.ToInt64((byte[])reader.GetValue(ordinal), 0); 171 | } 172 | 173 | public static long? GetInt64FromRowVersionNullable(this SqlDataReader reader, int ordinal) 174 | { 175 | if (reader.IsDBNull(ordinal)) 176 | return null; 177 | 178 | return BitConverter.ToInt64((byte[])reader.GetValue(ordinal), 0); 179 | } 180 | 181 | public static string GetBase64StringFromRowVersion(this SqlDataReader reader, int ordinal) 182 | { 183 | if (reader.IsDBNull(ordinal)) 184 | return null; 185 | 186 | return (Convert.ToBase64String((byte[])reader.GetValue(ordinal))); 187 | } 188 | 189 | public static byte[] GetBytesNullable(this SqlDataReader reader, int ordinal) 190 | { 191 | if (reader.IsDBNull(ordinal)) 192 | return null; 193 | 194 | return (byte[])reader.GetValue(ordinal); 195 | } 196 | 197 | public static byte[] GetByteArrayFromString(this SqlDataReader reader, int ordinal) 198 | { 199 | if (reader.IsDBNull(ordinal)) 200 | return Array.Empty(); 201 | 202 | var ids = reader.GetStringNullable(ordinal); 203 | 204 | if (string.IsNullOrWhiteSpace(ids)) 205 | return Array.Empty(); 206 | 207 | return ids.Split(',').Select(s => Convert.ToByte(s)).ToArray(); 208 | } 209 | 210 | public static short[] GetInt16ArrayFromString(this SqlDataReader reader, int ordinal) 211 | { 212 | if (reader.IsDBNull(ordinal)) 213 | return Array.Empty(); 214 | 215 | var ids = reader.GetString(ordinal); 216 | 217 | if (string.IsNullOrWhiteSpace(ids)) 218 | return Array.Empty(); 219 | 220 | return ids.Split(',').Select(s => Convert.ToInt16(s)).ToArray(); 221 | } 222 | 223 | public static int[] GetInt32ArrayFromString(this SqlDataReader reader, int ordinal) 224 | { 225 | if (reader.IsDBNull(ordinal)) 226 | return Array.Empty(); 227 | 228 | var ids = reader.GetString(ordinal); 229 | 230 | if (string.IsNullOrWhiteSpace(ids)) 231 | return Array.Empty(); 232 | 233 | return ids.Split(',').Select(s => Convert.ToInt32(s)).ToArray(); 234 | } 235 | 236 | public static SqlXml GetSqlXmlNullable(this SqlDataReader reader, int ordinal) 237 | { 238 | return reader.IsDBNull(ordinal) ? default : reader.GetSqlXml(ordinal); 239 | } 240 | 241 | public static SqlXml GetSqlXml(this SqlDataReader reader, int ordinal, SqlXml defaultValue) 242 | { 243 | return reader.IsDBNull(ordinal) ? defaultValue : reader.GetSqlXml(ordinal); 244 | } 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /Artisan.Orm/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Artisan.Orm 2 | { 3 | 4 | public static class StringExtensions 5 | { 6 | public static string TrimToNull(this string value) 7 | { 8 | if (value == null || value.Trim().Length == 0) return null; 9 | return value.Trim(); 10 | } 11 | 12 | public static string TruncateTo(this string value, int length) { 13 | if (value == null) return null; 14 | if (value.Length <= length) return value; 15 | return value[..length]; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Artisan.Orm/TreeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Artisan.Orm 6 | { 7 | 8 | // https://github.com/lobodava/artisan-orm/wiki/INode-Interface-and-ToTree-Methods 9 | 10 | public static class TreeExtensions 11 | { 12 | public static IList ToTreeList(this IEnumerable nodes, bool hierarchicallySorted = false) where T: class, INode 13 | { 14 | if (hierarchicallySorted) 15 | return ConvertHierarchicallySortedNodeListToTrees(nodes); 16 | 17 | return ConvertHierarchicallyUnsortedNodeListToTrees(nodes); 18 | } 19 | 20 | public static IList ToTreeList( 21 | this IEnumerable nodes, 22 | Func idSelector, 23 | Func parentIdSelector, 24 | Action linkParentAndNodeAction, 25 | bool hierarchicallySorted = false 26 | ) 27 | where TNode: class 28 | where TId: struct 29 | { 30 | if (hierarchicallySorted) 31 | return ConvertHierarchicallySortedNodeListToTrees(nodes, idSelector, parentIdSelector, linkParentAndNodeAction); 32 | 33 | return ConvertHierarchicallyUnsortedNodeListToTrees(nodes, idSelector, parentIdSelector, linkParentAndNodeAction); 34 | } 35 | 36 | 37 | 38 | 39 | public static T ToTree(this IEnumerable nodes, bool hierarchicallySorted = false) where T: class, INode 40 | { 41 | if (hierarchicallySorted) 42 | return ConvertHierarchicallySortedNodeListToTrees(nodes).FirstOrDefault(); 43 | 44 | return ConvertHierarchicallyUnsortedNodeListToTrees(nodes).FirstOrDefault(); 45 | } 46 | 47 | public static TNode ToTree( 48 | this IEnumerable nodes, 49 | Func idSelector, 50 | Func parentIdSelector, 51 | Action linkParentAndNodeAction, 52 | bool hierarchicallySorted = false 53 | ) 54 | where TNode: class 55 | where TId: struct 56 | { 57 | if (hierarchicallySorted) 58 | return ConvertHierarchicallySortedNodeListToTrees(nodes, idSelector, parentIdSelector, linkParentAndNodeAction).FirstOrDefault(); 59 | 60 | return ConvertHierarchicallyUnsortedNodeListToTrees(nodes, idSelector, parentIdSelector, linkParentAndNodeAction).FirstOrDefault(); 61 | } 62 | 63 | 64 | 65 | // this method allows to build a tree for one only iteration through the hierarchycally sorted node list 66 | 67 | private static IList ConvertHierarchicallySortedNodeListToTrees(IEnumerable nodes) where T: class, INode 68 | { 69 | var parentStack = new Stack(); 70 | var parent = default(T); 71 | var prevNode = default(T); 72 | var rootNodes = new List(); 73 | 74 | foreach (var node in nodes) 75 | { 76 | if (parent == null || node.ParentId == null) 77 | { 78 | rootNodes.Add(node); 79 | 80 | parent = node; 81 | } 82 | else if (node.ParentId == parent.Id) 83 | { 84 | parent.Children ??= new List(); 85 | 86 | parent.Children.Add(node); 87 | } 88 | else if (node.ParentId == prevNode.Id) 89 | { 90 | parentStack.Push(parent); 91 | 92 | parent = prevNode; 93 | 94 | parent.Children ??= new List(); 95 | 96 | parent.Children.Add(node); 97 | } 98 | else 99 | { 100 | var parentFound = false; 101 | 102 | while(parentStack.Count > 0 && parentFound == false) 103 | { 104 | parent = parentStack.Pop(); 105 | 106 | if (node.ParentId.Value == parent.Id) 107 | { 108 | parent.Children.Add(node); 109 | parentFound = true; 110 | } 111 | } 112 | 113 | if (parentFound == false) 114 | { 115 | rootNodes.Add(node); 116 | 117 | parent = node; 118 | } 119 | } 120 | 121 | prevNode = node; 122 | } 123 | 124 | return rootNodes; 125 | } 126 | 127 | 128 | private static IList ConvertHierarchicallySortedNodeListToTrees 129 | ( 130 | IEnumerable nodes, 131 | Func idSelector, 132 | Func parentIdSelector, 133 | Action linkParentAndNodeAction 134 | ) 135 | where TNode: class 136 | where TId: struct 137 | { 138 | var parentStack = new Stack(); 139 | var parent = default(TNode); 140 | var prevNode = default(TNode); 141 | var rootNodes = new List(); 142 | 143 | foreach (var node in nodes) 144 | { 145 | var parentId = parentIdSelector(node); 146 | 147 | 148 | if (parent == null || parentId == null) 149 | { 150 | rootNodes.Add(node); 151 | 152 | parent = node; 153 | } 154 | else if (parentId.Equals(idSelector(parent))) 155 | { 156 | linkParentAndNodeAction(parent, node); 157 | } 158 | else if (parentId.Equals(idSelector(prevNode))) 159 | { 160 | parentStack.Push(parent); 161 | 162 | parent = prevNode; 163 | 164 | linkParentAndNodeAction(parent, node); 165 | } 166 | else 167 | { 168 | var parentFound = false; 169 | 170 | while(parentStack.Count > 0 && parentFound == false) 171 | { 172 | parent = parentStack.Pop(); 173 | 174 | if (parentId.Equals(idSelector(parent))) 175 | { 176 | linkParentAndNodeAction(parent, node); 177 | 178 | parentFound = true; 179 | } 180 | } 181 | 182 | if (parentFound == false) 183 | { 184 | rootNodes.Add(node); 185 | 186 | parent = node; 187 | } 188 | } 189 | 190 | prevNode = node; 191 | } 192 | 193 | return rootNodes; 194 | } 195 | 196 | 197 | private static IList ConvertHierarchicallyUnsortedNodeListToTrees(IEnumerable nodes) where T: class, INode 198 | { 199 | var dictionary = nodes.ToDictionary(n => n.Id, n => n); 200 | var rootNodes = new List(); 201 | 202 | foreach (var node in dictionary.Select(item => item.Value)) 203 | { 204 | if (node.ParentId.HasValue && dictionary.TryGetValue(node.ParentId.Value, out T parent)) 205 | { 206 | parent.Children ??= new List(); 207 | 208 | parent.Children.Add(node); 209 | } 210 | else 211 | { 212 | rootNodes.Add(node); 213 | } 214 | } 215 | 216 | return rootNodes; 217 | } 218 | 219 | 220 | private static IList ConvertHierarchicallyUnsortedNodeListToTrees 221 | ( 222 | IEnumerable nodes, 223 | Func idSelector, 224 | Func parentIdSelector, 225 | Action linkParentAndNodeAction 226 | ) 227 | where TNode: class 228 | where TId: struct 229 | { 230 | var dictionary = nodes.ToDictionary(idSelector, n => n); 231 | var rootNodes = new List(); 232 | 233 | foreach (var node in dictionary.Select(item => item.Value)) 234 | { 235 | var paretnId = parentIdSelector(node); 236 | 237 | if (paretnId.HasValue && dictionary.TryGetValue(paretnId.Value, out TNode parent)) 238 | { 239 | linkParentAndNodeAction(parent, node); 240 | } 241 | else 242 | { 243 | rootNodes.Add(node); 244 | } 245 | } 246 | 247 | return rootNodes; 248 | } 249 | 250 | } 251 | 252 | } 253 | -------------------------------------------------------------------------------- /Artisan.Orm/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Artisan.Orm 5 | { 6 | // taken from http://stackoverflow.com/a/15578098/623190 7 | 8 | internal static class TypeExtensions 9 | { 10 | private static readonly Type[] SimpleTypes; 11 | 12 | static TypeExtensions() 13 | { 14 | var types = new[] 15 | { 16 | //typeof (Enum), 17 | typeof (String), 18 | typeof (Char), 19 | typeof (Guid), 20 | 21 | typeof (Boolean), 22 | typeof (Byte), 23 | typeof (Int16), 24 | typeof (Int32), 25 | typeof (Int64), 26 | typeof (Single), 27 | typeof (Double), 28 | typeof (Decimal), 29 | 30 | typeof (SByte), 31 | typeof (UInt16), 32 | typeof (UInt32), 33 | typeof (UInt64), 34 | 35 | typeof (DateTime), 36 | typeof (DateTimeOffset), 37 | typeof (TimeSpan), 38 | }; 39 | 40 | var nullTypes = types 41 | .Where(t => t.IsValueType) 42 | .Select(t => typeof (Nullable<>) 43 | .MakeGenericType(t)); 44 | 45 | SimpleTypes = types.Concat(nullTypes).ToArray(); 46 | } 47 | 48 | internal static bool IsSimpleType(this Type type) 49 | { 50 | if (SimpleTypes.Any(x => x.IsAssignableFrom(type))) 51 | return true; 52 | 53 | var nut = Nullable.GetUnderlyingType(type); 54 | return nut != null && nut.IsEnum; 55 | } 56 | 57 | internal static bool IsNullableValueType(this Type type) 58 | { 59 | //type.IsValueType && Nullable.GetUnderlyingType(type) != null || type == typeof(String) 60 | 61 | return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) 62 | || type == typeof(String); 63 | } 64 | 65 | 66 | internal static Type GetUnderlyingType(this Type type) 67 | { 68 | return Nullable.GetUnderlyingType(type) ?? type; 69 | } 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Artisan.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33516.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artisan.Orm", "Artisan.Orm\Artisan.Orm.csproj", "{BA18C65B-49A6-4C98-BCC5-B71CB57F16FF}" 7 | EndProject 8 | Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "Database", "Database\Database.sqlproj", "{13350295-4FBD-470D-9258-AAC77094309A}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{F1EE12B6-5A00-4021-8FDA-ED6B0C6B272F}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {BA18C65B-49A6-4C98-BCC5-B71CB57F16FF} = {BA18C65B-49A6-4C98-BCC5-B71CB57F16FF} 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {BA18C65B-49A6-4C98-BCC5-B71CB57F16FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {BA18C65B-49A6-4C98-BCC5-B71CB57F16FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {BA18C65B-49A6-4C98-BCC5-B71CB57F16FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {BA18C65B-49A6-4C98-BCC5-B71CB57F16FF}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {13350295-4FBD-470D-9258-AAC77094309A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {13350295-4FBD-470D-9258-AAC77094309A}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {13350295-4FBD-470D-9258-AAC77094309A}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 28 | {13350295-4FBD-470D-9258-AAC77094309A}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {13350295-4FBD-470D-9258-AAC77094309A}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {13350295-4FBD-470D-9258-AAC77094309A}.Release|Any CPU.Deploy.0 = Release|Any CPU 31 | {F1EE12B6-5A00-4021-8FDA-ED6B0C6B272F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {F1EE12B6-5A00-4021-8FDA-ED6B0C6B272F}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {F1EE12B6-5A00-4021-8FDA-ED6B0C6B272F}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {F1EE12B6-5A00-4021-8FDA-ED6B0C6B272F}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {2D42EB03-37FB-4FEE-A347-8C2347A14D64} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /Database/1. Tables/Records/1. GrandRecords.sql: -------------------------------------------------------------------------------- 1 | create table dbo.GrandRecords 2 | ( 3 | Id int not null identity, 4 | [Name] varchar(30) not null , 5 | 6 | constraint PK_Grandparents primary key clustered (Id), 7 | ); 8 | -------------------------------------------------------------------------------- /Database/1. Tables/Records/2. Records.sql: -------------------------------------------------------------------------------- 1 | create table dbo.Records 2 | ( 3 | Id int not null identity, 4 | GrandRecordId int not null , 5 | [Name] varchar(30) not null , 6 | RecordTypeId tinyint null , 7 | Number smallint null , 8 | [Date] datetime2(0) null , 9 | Amount decimal(19,2) null , 10 | IsActive bit null , 11 | Comment nvarchar(500) null , 12 | 13 | constraint PK_Records primary key clustered (Id), 14 | 15 | constraint FK_GrandRecords_GrandRecordId foreign key (GrandRecordId) references dbo.GrandRecords (Id) on delete cascade, 16 | 17 | constraint FK_GrandRecords_RecordTypeId foreign key (RecordTypeId) references dbo.RecordTypes (Id) 18 | ); 19 | 20 | GO 21 | 22 | create nonclustered index IX_GrandRecordId on dbo.Records 23 | ( 24 | GrandRecordId asc 25 | ); 26 | -------------------------------------------------------------------------------- /Database/1. Tables/Records/3. ChildRecords.sql: -------------------------------------------------------------------------------- 1 | create table dbo.ChildRecords 2 | ( 3 | Id int not null identity, 4 | RecordId int not null , 5 | [Name] varchar(30) not null , 6 | 7 | constraint PK_ChildRecords primary key clustered (Id), 8 | 9 | constraint FK_ChildRecords_RecordId foreign key (RecordId) references dbo.Records (Id) on delete cascade 10 | ); 11 | 12 | GO 13 | 14 | create nonclustered index IX_RecordId on dbo.ChildRecords 15 | ( 16 | RecordId asc 17 | ); 18 | -------------------------------------------------------------------------------- /Database/1. Tables/Records/4. RecordTypes.sql: -------------------------------------------------------------------------------- 1 | create table dbo.RecordTypes 2 | ( 3 | Id tinyint not null , 4 | Code varchar(30) not null , 5 | [Name] nvarchar(50) not null , 6 | 7 | constraint PK_RecordTypes primary key clustered (Id), 8 | 9 | constraint UQ_RecordTypes_Code unique (Code), 10 | 11 | constraint UQ_RecordTypes_Name unique (Name), 12 | ); 13 | -------------------------------------------------------------------------------- /Database/1. Tables/Users/1. Users.sql: -------------------------------------------------------------------------------- 1 | create sequence dbo.UserId as int minvalue 1 start with 1 increment by 1 no cache; 2 | 3 | GO 4 | 5 | create table dbo.Users 6 | ( 7 | Id int not null constraint DF_UserId default next value for dbo.UserId, 8 | [Login] varchar(20) not null , 9 | [Name] nvarchar(50) not null , 10 | Email varchar(50) not null , 11 | [RowVersion] rowversion not null , 12 | 13 | constraint PK_Users primary key clustered (Id), 14 | 15 | constraint UQ_Users_Login unique ([Login]), 16 | 17 | constraint UQ_Users_Email unique (Email), 18 | ); 19 | -------------------------------------------------------------------------------- /Database/1. Tables/Users/2. Roles.sql: -------------------------------------------------------------------------------- 1 | create table dbo.Roles 2 | ( 3 | Id tinyint not null , 4 | Code varchar(30) not null , 5 | [Name] nvarchar(50) not null , 6 | 7 | constraint PK_Roles primary key clustered (Id), 8 | 9 | constraint UQ_Roles_Code unique (Code), 10 | 11 | constraint UQ_Roles_Name unique (Name), 12 | ); 13 | -------------------------------------------------------------------------------- /Database/1. Tables/Users/3. UserRoles.sql: -------------------------------------------------------------------------------- 1 | create table dbo.UserRoles 2 | ( 3 | UserId int not null, 4 | RoleId tinyint not null, 5 | 6 | constraint PK_UserRoles primary key clustered (UserId, RoleId), 7 | 8 | constraint FK_UserRoles_UserId foreign key (UserId) references dbo.Users (Id) on delete cascade, 9 | 10 | constraint FK_UserRoles_RoleId foreign key (RoleId) references dbo.Roles (Id) on delete cascade 11 | ); 12 | -------------------------------------------------------------------------------- /Database/1. Tables/Users/4. Folders.sql: -------------------------------------------------------------------------------- 1 | create sequence dbo.FolderId as int minvalue 1 start with 1 increment by 1 no cache; 2 | 3 | GO 4 | 5 | create table dbo.Folders 6 | ( 7 | UserId int not null , 8 | Hid hierarchyid not null , 9 | Id int not null constraint DF_FolderId default next value for dbo.FolderId, 10 | ParentId int null , 11 | [Name] nvarchar(50) not null , 12 | 13 | constraint CU_Folders unique clustered (UserId asc, Hid asc), 14 | 15 | constraint PK_Folders primary key nonclustered (Id asc), 16 | 17 | constraint FK_Folders_UserId foreign key (UserId) references dbo.Users (Id), 18 | 19 | constraint FK_Folders_ParentId foreign key (ParentId) references dbo.Folders (Id), 20 | 21 | constraint CH_Folders_ParentId check (Hid = 0x and ParentId is null or Hid <> 0x and ParentId is not null) 22 | ); 23 | 24 | GO 25 | 26 | create unique nonclustered index CI_ParentId on dbo.Folders ( 27 | ParentId, 28 | Name, 29 | Id 30 | ); 31 | -------------------------------------------------------------------------------- /Database/2. Views/vwChildRecords.sql: -------------------------------------------------------------------------------- 1 | create view dbo.vwChildRecords 2 | as 3 | ( 4 | select 5 | Id , 6 | RecordId , 7 | [Name] 8 | from 9 | dbo.ChildRecords 10 | ); 11 | -------------------------------------------------------------------------------- /Database/2. Views/vwGrandRecords.sql: -------------------------------------------------------------------------------- 1 | create view dbo.vwGrandRecords 2 | as 3 | ( 4 | select 5 | Id , 6 | [Name] 7 | from 8 | dbo.GrandRecords 9 | ); 10 | -------------------------------------------------------------------------------- /Database/2. Views/vwRecords.sql: -------------------------------------------------------------------------------- 1 | create view dbo.vwRecords 2 | as 3 | ( 4 | select 5 | Id , 6 | GrandRecordId , 7 | [Name] , 8 | RecordTypeId , 9 | Number , 10 | [Date] , 11 | Amount , 12 | IsActive , 13 | Comment 14 | from 15 | dbo.Records 16 | ); 17 | -------------------------------------------------------------------------------- /Database/2. Views/vwRecordsWithTypes.sql: -------------------------------------------------------------------------------- 1 | create view dbo.vwRecordsWithTypes 2 | as 3 | ( 4 | select 5 | r.Id , 6 | GrandRecordId , 7 | r.[Name] , 8 | RecordTypeId , 9 | Number , 10 | [Date] , 11 | Amount , 12 | IsActive , 13 | Comment , 14 | 15 | rt_Id = rt.Id , 16 | rt_Code = rt.Code , 17 | rt_Name = rt.[Name] 18 | 19 | from 20 | dbo.Records r 21 | left join dbo.RecordTypes rt on rt.Id = r.RecordTypeId 22 | ); 23 | -------------------------------------------------------------------------------- /Database/2. Views/vwUsers.sql: -------------------------------------------------------------------------------- 1 | create view dbo.vwUsers 2 | as 3 | ( 4 | select 5 | Id , 6 | [Login] , 7 | [Name] , 8 | Email , 9 | [RowVersion] , 10 | RoleIds = dbo.GetUserRoleIds(Id) 11 | from 12 | dbo.Users u 13 | ); 14 | -------------------------------------------------------------------------------- /Database/3. Functions/GetHidCode.sql: -------------------------------------------------------------------------------- 1 | create function dbo.GetHidCode 2 | ( 3 | @Hid hierarchyid 4 | ) 5 | returns varchar(1000) 6 | as 7 | begin 8 | 9 | return replace(convert(varchar(1000), cast(@Hid as varbinary(892)), 2), '0x', ''); 10 | 11 | end; 12 | -------------------------------------------------------------------------------- /Database/3. Functions/GetUserRoleIds.sql: -------------------------------------------------------------------------------- 1 | create function dbo.GetUserRoleIds 2 | ( 3 | @UserId int 4 | ) 5 | returns varchar(100) 6 | as 7 | begin 8 | 9 | declare @UserRoleIds varchar(100); 10 | 11 | select 12 | @UserRoleIds = concat(coalesce(@UserRoleIds + ',', ''), RoleId) 13 | from 14 | dbo.UserRoles 15 | where 16 | UserId = @UserId; 17 | 18 | return @UserRoleIds; 19 | end 20 | -------------------------------------------------------------------------------- /Database/3. Functions/Utility Functions/GetErrorMessage.sql: -------------------------------------------------------------------------------- 1 | create function dbo.GetErrorMessage 2 | ( 3 | ) 4 | returns nvarchar(4000) 5 | as 6 | begin 7 | return N'An error occurred in the stored procedure ' + error_procedure() + ' ' 8 | + N'on line ' + cast(error_line() as varchar(10)) + '. ' 9 | + N'Error number: ' + cast(error_number() as varchar(10)) + '. ' 10 | + N'Error message: ' + error_message() + '.'; 11 | end; 12 | -------------------------------------------------------------------------------- /Database/3. Functions/Utility Functions/SplitIntIds.sql: -------------------------------------------------------------------------------- 1 | create function SplitIntIds ( --[dbo].[DelimitedSplit8K] -- http://www.sqlservercentral.com/articles/Tally+Table/72993/ 2 | @Ids varchar(1000) 3 | ) 4 | returns table with schemabinding as 5 | return 6 | ( 7 | with N as 8 | ( 9 | select n from (values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) t(n) 10 | ), 11 | E3 as 12 | ( 13 | select-- top 8000 14 | row_number() over(order by (select null)) as number 15 | from 16 | N n1, N n2, N n3--, N n4 17 | ), 18 | cteTally(N) as 19 | ( 20 | select top (isnull(datalength(@Ids), 0)) row_number() over (order by (select null)) from E3 21 | ), 22 | cteStart(N1) as 23 | ( 24 | select 1 union all 25 | select t.N + 1 from cteTally t where substring(@Ids, t.N, 1) = ',' 26 | ), 27 | cteLen(N1,L1) as 28 | ( 29 | select 30 | s.N1, 31 | isnull(nullif(charindex(',', @Ids, s.N1), 0) - s.N1, 8000) 32 | from 33 | cteStart s 34 | ) 35 | select distinct 36 | Id = cast(substring(@Ids, l.N1, l.L1) as int) 37 | from 38 | cteLen l 39 | where 40 | len(substring(@Ids, l.N1, l.L1)) > 0 41 | ); 42 | -------------------------------------------------------------------------------- /Database/3. Functions/Utility Functions/SplitSmallIntIds.sql: -------------------------------------------------------------------------------- 1 | create function SplitSmallIntIds ( --[dbo].[DelimitedSplit8K] -- http://www.sqlservercentral.com/articles/Tally+Table/72993/ 2 | @Ids varchar(1000) 3 | ) 4 | returns table with schemabinding as 5 | return 6 | ( 7 | with N as 8 | ( 9 | select n from (values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) t(n) 10 | ), 11 | E3 as 12 | ( 13 | select-- top 8000 14 | row_number() over(order by (select null)) as number 15 | from 16 | N n1, N n2, N n3--, N n4 17 | ), 18 | cteTally(N) as 19 | ( 20 | select top (isnull(datalength(@Ids), 0)) row_number() over (order by (select null)) from E3 21 | ), 22 | cteStart(N1) as 23 | ( 24 | select 1 union all 25 | select t.N + 1 from cteTally t where substring(@Ids, t.N, 1) = ',' 26 | ), 27 | cteLen(N1,L1) as 28 | ( 29 | select 30 | s.N1, 31 | isnull(nullif(charindex(',', @Ids, s.N1), 0) - s.N1, 8000) 32 | from 33 | cteStart s 34 | ) 35 | select distinct 36 | Id = cast(substring(@Ids, l.N1, l.L1) as smallint) 37 | from 38 | cteLen l 39 | where 40 | len(substring(@Ids, l.N1, l.L1)) > 0 41 | ); 42 | -------------------------------------------------------------------------------- /Database/3. Functions/Utility Functions/SplitTinyIntIds.sql: -------------------------------------------------------------------------------- 1 | create function SplitTinyIntIds ( --[dbo].[DelimitedSplit8K] -- http://www.sqlservercentral.com/articles/Tally+Table/72993/ 2 | @Ids varchar(1000) 3 | ) 4 | returns table with schemabinding as 5 | return 6 | ( 7 | with N as 8 | ( 9 | select n from (values (1),(2),(3),(4),(5),(6),(7),(8),(9),(10)) t(n) 10 | ), 11 | E3 as 12 | ( 13 | select-- top 8000 14 | row_number() over(order by (select null)) as number 15 | from 16 | N n1, N n2, N n3--, N n4 17 | ), 18 | cteTally(N) as 19 | ( 20 | select top (isnull(datalength(@Ids), 0)) row_number() over (order by (select null)) from E3 21 | ), 22 | cteStart(N1) as 23 | ( 24 | select 1 union all 25 | select t.N + 1 from cteTally t where substring(@Ids, t.N, 1) = ',' 26 | ), 27 | cteLen(N1,L1) as 28 | ( 29 | select 30 | s.N1, 31 | isnull(nullif(charindex(',', @Ids, s.N1), 0) - s.N1, 1000) 32 | from 33 | cteStart s 34 | ) 35 | select distinct 36 | Id = cast(substring(@Ids, l.N1, l.L1) as tinyint) 37 | from 38 | cteLen l 39 | where 40 | len(substring(@Ids, l.N1, l.L1)) > 0 41 | ); 42 | -------------------------------------------------------------------------------- /Database/4. Types/ChildRecordTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.ChildRecordTableType as table 2 | ( 3 | Id int not null primary key clustered, 4 | RecordId int not null , 5 | [Name] varchar(30) not null 6 | ); 7 | -------------------------------------------------------------------------------- /Database/4. Types/Common/BigIntIdTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.BigIntIdTableType as table 2 | ( 3 | Id bigint not null primary key clustered 4 | ); 5 | -------------------------------------------------------------------------------- /Database/4. Types/Common/DataMessageTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.DataReplyMessageTableType as table 2 | ( 3 | Code varchar(50) not null , 4 | [Text] nvarchar(4000) null , 5 | Id bigint null , 6 | [Value] sql_variant null , 7 | 8 | unique (Code, Id) 9 | ); 10 | -------------------------------------------------------------------------------- /Database/4. Types/Common/IntIdTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.IntIdTableType as table 2 | ( 3 | Id int not null primary key clustered 4 | ); 5 | -------------------------------------------------------------------------------- /Database/4. Types/Common/SmallIntIdTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.SmallIntIdTableType as table 2 | ( 3 | Id smallint not null primary key clustered 4 | ); 5 | -------------------------------------------------------------------------------- /Database/4. Types/Common/TinyIntIdTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.TinyIntIdTableType as table 2 | ( 3 | Id tinyint not null primary key clustered 4 | ); 5 | -------------------------------------------------------------------------------- /Database/4. Types/FolderTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.FolderTableType as table 2 | ( 3 | Id int not null primary key clustered, 4 | ParentId int not null , 5 | [Name] nvarchar(50) not null 6 | ); 7 | -------------------------------------------------------------------------------- /Database/4. Types/GrandRecordTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.GrandRecordTableType as table 2 | ( 3 | Id int not null primary key clustered, 4 | [Name] varchar(30) not null 5 | ); 6 | -------------------------------------------------------------------------------- /Database/4. Types/RecordTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.RecordTableType as table 2 | ( 3 | Id int not null primary key clustered, 4 | GrandRecordId int not null , 5 | [Name] varchar(30) not null , 6 | RecordTypeId tinyint null , 7 | Number smallint null , 8 | [Date] datetime2 null , 9 | Amount decimal(19,2) null , 10 | IsActive bit null , 11 | Comment nvarchar(500) null 12 | ); 13 | -------------------------------------------------------------------------------- /Database/4. Types/UserTableType.sql: -------------------------------------------------------------------------------- 1 | create type dbo.UserTableType as table 2 | ( 3 | Id int not null primary key clustered, 4 | [Login] varchar(20) not null , 5 | [Name] nvarchar(50) not null , 6 | Email varchar(50) not null , 7 | [RowVersion] binary(8) null , 8 | RoleIds varchar(100) null 9 | ); 10 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/DeleteFolder.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.DeleteFolder 2 | @FolderId int, 3 | @WithHidReculc bit = 0 4 | as 5 | begin 6 | set nocount on; 7 | 8 | declare 9 | @ParentId int , 10 | @UserId int , 11 | @ParentHid hierarchyid , 12 | @StartTranCount int ; 13 | 14 | begin try 15 | set @StartTranCount = @@trancount; 16 | if @StartTranCount = 0 17 | begin 18 | set transaction isolation level serializable; 19 | begin transaction; 20 | end; 21 | 22 | 23 | begin -- init variables 24 | 25 | select 26 | @ParentId = ParentId , 27 | @UserId = UserId 28 | from 29 | dbo.Folders 30 | where 31 | Id = @FolderId; 32 | 33 | select 34 | @ParentHid = Hid 35 | from 36 | dbo.Folders 37 | where 38 | Id = @ParentId; 39 | 40 | end; 41 | 42 | 43 | begin -- delete folder and its descendants 44 | 45 | delete d 46 | from 47 | dbo.Folders f 48 | inner join dbo.Folders d on d.UserId = @UserId and d.Hid.IsDescendantOf(f.Hid) = 1 49 | where 50 | f.Id = @FolderId; 51 | 52 | 53 | if @WithHidReculc = 1 54 | exec dbo.RecalcSubFolderHids @UserId, @ParentId, @ParentHid; 55 | 56 | end; 57 | 58 | if @StartTranCount = 0 commit transaction; 59 | 60 | end try 61 | begin catch 62 | if xact_state() <> 0 and @StartTranCount = 0 rollback transaction; 63 | 64 | declare @ErrorMessage nvarchar(4000) = dbo.GetErrorMessage(); 65 | raiserror (@ErrorMessage, 16, 1); 66 | return; 67 | end catch; 68 | 69 | end; 70 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/FindFoldersWithParents.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.FindFoldersWithParents 2 | @UserId int, 3 | @Level smallint = null, 4 | @FolderName nvarchar(50) = null 5 | as 6 | begin 7 | set nocount on; 8 | 9 | declare @Hids table (Hid hierarchyid primary key); 10 | 11 | insert into @Hids 12 | select 13 | Hid 14 | from 15 | dbo.Folders 16 | where 17 | UserId = @UserId 18 | and (@Level is null or Hid.GetLevel() = @Level) 19 | and (@FolderName is null or Name like '%' + @FolderName + '%'); 20 | 21 | 22 | select distinct 23 | d.Id , 24 | d.ParentId , 25 | d.[Name] , 26 | [Level] = d.Hid.GetLevel(), 27 | HidCode = dbo.GetHidCode(d.Hid), 28 | HidPath = d.Hid.ToString() 29 | from 30 | @Hids h 31 | inner join dbo.Folders d on h.Hid.IsDescendantOf(d.Hid) = 1 32 | where 33 | UserId = @UserId 34 | order by 35 | HidCode; 36 | 37 | end; 38 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GenerateFolders.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GenerateFolders 2 | 3 | as 4 | begin 5 | 6 | declare @Letters table (Letter char(1)); 7 | 8 | declare @ParentId bigint = null; 9 | 10 | 11 | declare @PowerIndex int = 3; -- 1,092 records 12 | 13 | --declare @PowerIndex int = 5; -- 19,530 records 14 | 15 | --declare @PowerIndex int = 10; -- 1,111,110 records, ~ 2 minutes 16 | 17 | 18 | insert into @Letters 19 | select top (@PowerIndex) l 20 | from (values 21 | ('A'),('B'),('C'),('D'),('E') 22 | ,('F'),('G'),('H'),('I'),('J') 23 | ,('K'),('L'),('M'),('N'),('O') 24 | ,('P'),('Q'),('R'),('S'),('T') 25 | ,('U'),('V'),('W'),('X'),('Y'),('Z') 26 | ) L(l); 27 | 28 | drop table if exists #Hierarchy ; 29 | 30 | create table #Hierarchy 31 | ( 32 | UserId int not null , 33 | Hid hierarchyid null , 34 | Id int not null identity, 35 | ParentId int null , 36 | Name nvarchar(50) not null , 37 | Level tinyint not null 38 | ) 39 | 40 | insert into #Hierarchy ( 41 | UserId , 42 | Hid , 43 | Name , 44 | Level ) 45 | select 46 | UserId = u.Id , 47 | Hid = '/' , 48 | Name = '', 49 | Level = 0 50 | from 51 | dbo.Users u 52 | where 53 | u.Id <= @PowerIndex; 54 | 55 | 56 | insert into #Hierarchy ( 57 | ParentId , 58 | UserId , 59 | Name , 60 | Level ) 61 | select 62 | ParentId = h.Id, 63 | UserId = u.Id, 64 | Name = '1' + l.Letter, 65 | Level = 1 66 | from 67 | #Hierarchy h 68 | inner join dbo.Users u on u.Id = h.UserId 69 | cross join @Letters l 70 | where 71 | h.Level = 0 72 | and u.Id <= @PowerIndex; 73 | 74 | 75 | insert into #Hierarchy ( 76 | ParentId , 77 | UserId , 78 | Name , 79 | Level ) 80 | select 81 | ParentId = h.Id, 82 | UserId = u.Id, 83 | Name = '2' + l.Letter, 84 | Level = 2 85 | from 86 | #Hierarchy h 87 | inner join dbo.Users u on u.Id = h.UserId 88 | cross join @Letters l 89 | where 90 | h.Level = 1 91 | and u.Id <= @PowerIndex; 92 | 93 | 94 | 95 | insert into #Hierarchy ( 96 | ParentId , 97 | UserId , 98 | Name , 99 | Level ) 100 | select 101 | ParentId = h.Id, 102 | UserId = u.Id, 103 | Name = '3' + l.Letter, 104 | Level = 3 105 | from 106 | #Hierarchy h 107 | inner join dbo.Users u on u.Id = h.UserId 108 | cross join @Letters l 109 | where 110 | h.Level = 2 111 | and u.Id <= @PowerIndex; 112 | 113 | 114 | insert into #Hierarchy ( 115 | ParentId , 116 | UserId , 117 | Name , 118 | Level ) 119 | select 120 | ParentId = h.Id, 121 | UserId = u.Id, 122 | Name = '4' + l.Letter, 123 | Level = 4 124 | from 125 | #Hierarchy h 126 | inner join dbo.Users u on u.Id = h.UserId 127 | cross join @Letters l 128 | where 129 | h.Level = 3 130 | and u.Id <= @PowerIndex; 131 | 132 | 133 | 134 | insert into #Hierarchy ( 135 | ParentId , 136 | UserId , 137 | Name , 138 | Level ) 139 | select 140 | ParentId = h.Id, 141 | UserId = u.Id, 142 | Name = '5' + l.Letter, 143 | Level = 5 144 | from 145 | #Hierarchy h 146 | inner join dbo.Users u on u.Id = h.UserId 147 | cross join @Letters l 148 | where 149 | h.Level = 4 150 | and u.Id <= @PowerIndex; 151 | 152 | alter table #Hierarchy add constraint PK_Hierarchy primary key clustered (Id); 153 | 154 | create unique index UX_ParentId on #Hierarchy ( 155 | UserId, 156 | ParentId, 157 | Name, 158 | Id 159 | ); 160 | 161 | 162 | with Recursion as 163 | ( 164 | select 165 | UserId = UserId , 166 | Hid = cast('/' as varchar(1000)), 167 | Id = Id , 168 | ParentId = ParentId , 169 | [Name] = [Name] 170 | from 171 | #Hierarchy h 172 | where 173 | h.ParentId is null 174 | 175 | union all 176 | 177 | select 178 | UserId = h.UserId , 179 | Hid = cast(concat(r.Hid, row_number() over (partition by h.ParentId order by h.Name, h.Id), '/') as varchar(1000)), 180 | Id = h.Id , 181 | ParentId = h.ParentId , 182 | [Name] = h.[Name] 183 | from 184 | Recursion r 185 | inner join #Hierarchy h on h.UserId = r.UserId and h.ParentId = r.Id 186 | ) 187 | update h set 188 | Hid = r.Hid 189 | from 190 | #Hierarchy h 191 | inner join Recursion r on r.Id = h.Id 192 | where 193 | h.Hid is null or h.Hid <> r.Hid; 194 | 195 | alter table #Hierarchy drop constraint PK_Hierarchy; 196 | 197 | create unique clustered index CI_HId on #Hierarchy ( 198 | UserId, 199 | HId 200 | ); 201 | 202 | alter table #Hierarchy add constraint PK_Hierarchy primary key nonclustered (Id); 203 | 204 | 205 | truncate table dbo.Folders; 206 | 207 | insert into dbo.Folders ( 208 | UserId , 209 | Hid , 210 | Id , 211 | ParentId , 212 | Name 213 | ) 214 | select 215 | UserId , 216 | Hid , 217 | Id , 218 | ParentId , 219 | Name 220 | from 221 | #Hierarchy; 222 | 223 | 224 | declare @MaxId int, @q nvarchar(500); 225 | set @MaxId = isnull((select max(Id) from dbo.Folders), 0) + 1; 226 | set @q = concat('alter sequence dbo.FolderId restart with ', @MaxId); 227 | exec (@q); 228 | 229 | end; 230 | 231 | GO 232 | 233 | --exec dbo.ResetFolders; 234 | 235 | --select *, cast(Hid as varchar(1000)) StrHid from dbo.Folders order by UserId, Hid; -- UserId, ParentId, Name; 236 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetFolderById.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetFolderById 2 | @Id int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | declare @Path nvarchar(4000); 8 | 9 | select 10 | @Path = concat(coalesce(@Path, ''), a.[Name], ' › ') 11 | from 12 | dbo.Folders f 13 | inner join dbo.Folders a on a.UserId = f.UserId and f.Hid.IsDescendantOf(a.Hid) = 1 14 | where 15 | f.Id = @Id and a.Id <> @Id 16 | order by 17 | a.Hid; 18 | 19 | 20 | select 21 | Id , 22 | ParentId , 23 | [Name] , 24 | [Level] = Hid.GetLevel(), 25 | HidCode = dbo.GetHidCode(Hid), 26 | HidPath = Hid.ToString(), 27 | [Path] = @Path 28 | from 29 | dbo.Folders 30 | where 31 | Id = @Id 32 | end; 33 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetFolderParents.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetFolderWithParents 2 | @FolderId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | select 8 | a.Id , 9 | a.ParentId , 10 | a.Name , 11 | [Level] = a.Hid.GetLevel(), 12 | HidCode = dbo.GetHidCode(a.Hid), 13 | HidPath = a.Hid.ToString() 14 | from 15 | dbo.Folders f 16 | inner join dbo.Folders a on f.UserId = a.UserId and f.Hid.IsDescendantOf(a.Hid) = 1 17 | where 18 | f.Id = @FolderId 19 | order by 20 | a.Hid; 21 | 22 | end; 23 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetFolderWithSubFolders.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetFolderWithSubFolders 2 | @FolderId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | select 8 | d.Id , 9 | d.ParentId , 10 | d.[Name] , 11 | [Level] = d.Hid.GetLevel(), 12 | HidCode = dbo.GetHidCode(d.Hid), 13 | HidPath = d.Hid.ToString() 14 | from 15 | dbo.Folders f 16 | inner join dbo.Folders d on d.UserId = f.UserId and d.Hid.IsDescendantOf(f.Hid) = 1 17 | where 18 | f.Id = @FolderId 19 | order by 20 | d.Hid; 21 | end; 22 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetImmediateSubFolders.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetImmediateSubFolders 2 | @FolderId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | select 8 | Id , 9 | ParentId , 10 | [Name] , 11 | [Level] = Hid.GetLevel(), 12 | HidCode = dbo.GetHidCode(Hid), 13 | HidPath = Hid.ToString() 14 | from 15 | dbo.Folders 16 | where 17 | ParentId = @FolderId 18 | order by 19 | Hid; 20 | 21 | end; 22 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetNextSiblingFolder.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetNextSiblingFolder 2 | @FolderId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | declare @NextSiblingFolderId int; 8 | 9 | with cte as 10 | ( 11 | select 12 | FolderId = s.Id, 13 | NextSiblingFolderId = lead(s.Id) over(order by s.Hid) 14 | from 15 | dbo.Folders f 16 | inner join dbo.Folders s on s.ParentId = f.ParentId 17 | where 18 | f.Id = @FolderId 19 | ) 20 | select 21 | @NextSiblingFolderId = NextSiblingFolderId 22 | from 23 | cte 24 | where 25 | FolderId = @FolderId; 26 | 27 | 28 | exec dbo.GetFolderById @NextSiblingFolderId; 29 | 30 | end; 31 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetPreviousSiblingFolder.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetPreviousSiblingFolder 2 | @FolderId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | declare @PreviousSiblingFolderId int; 8 | 9 | with cte as 10 | ( 11 | select 12 | FolderId = s.Id, 13 | PreviousSiblingFolderId = lag(s.Id) over(order by s.Hid) 14 | from 15 | dbo.Folders f 16 | inner join dbo.Folders s on s.ParentId = f.ParentId 17 | where 18 | f.Id = @FolderId 19 | ) 20 | select 21 | @PreviousSiblingFolderId = PreviousSiblingFolderId 22 | from 23 | cte 24 | where 25 | FolderId = @FolderId; 26 | 27 | 28 | exec dbo.GetFolderById @PreviousSiblingFolderId; 29 | 30 | end; 31 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/GetUserRootFolder.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetUserRootFolder 2 | @UserId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | declare @RootHid hierarchyid = 0x; 8 | 9 | 10 | if not exists (select * from dbo.Folders where UserId = @UserId and Hid = @RootHid) 11 | begin 12 | 13 | insert into dbo.Folders ( 14 | UserId , 15 | Hid , 16 | ParentId , 17 | [Name] ) 18 | values ( 19 | @UserId , 20 | @RootHid , 21 | null , 22 | '' ); 23 | end; 24 | 25 | 26 | select 27 | Id , 28 | ParentId , 29 | [Name] , 30 | [Level] = Hid.GetLevel(), 31 | HidCode = '', 32 | HidPath = Hid.ToString(), 33 | [Path] = '' 34 | from 35 | dbo.Folders 36 | where 37 | UserId = @UserId and Hid = @RootHid 38 | end; 39 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/ReculcSubFolderHids.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.RecalcSubFolderHids 2 | @UserId int , 3 | @ParentId int , 4 | @ParentHid hierarchyid , 5 | @OldParentId int = null, 6 | @OldParentHid hierarchyid = null 7 | as 8 | begin 9 | 10 | declare @ParentHidStr varchar(1000) = cast(@ParentHid as varchar(1000)); 11 | declare @OldParentHidStr varchar(1000) = cast(@OldParentId as varchar(1000)); 12 | 13 | with Recursion as 14 | ( 15 | select 16 | Id , 17 | ParentId , 18 | [Name] , 19 | OldHid = cast(Hid as varchar(1000)), 20 | NewHid = cast( 21 | concat( 22 | case when ParentId = @ParentId then @ParentHidStr else @OldParentHidStr end, 23 | row_number() over (order by [Name], Id), 24 | '/' 25 | ) 26 | as varchar(1000) 27 | ) 28 | from 29 | dbo.Folders 30 | where 31 | ParentId in (@ParentId, @OldParentId) 32 | 33 | union all 34 | 35 | select 36 | Id = f.Id , 37 | ParentId = f.ParentId , 38 | [Name] = f.[Name] , 39 | OldHid = cast(f.Hid as varchar(1000)), 40 | NewHid = cast(concat(r.NewHid, row_number() over (partition by f.ParentId order by f.Name, f.Id), '/') as varchar(1000)) 41 | from 42 | Recursion r 43 | inner join dbo.Folders f on f.ParentId = r.Id 44 | where 45 | r.OldHid <> r.NewHid 46 | ) 47 | update f set 48 | Hid = r.NewHid 49 | from 50 | dbo.Folders f 51 | inner join Recursion r on r.Id = f.Id and f.Hid <> r.NewHid 52 | where 53 | f.UserId = @UserId 54 | 55 | option (recompile); 56 | 57 | end; 58 | -------------------------------------------------------------------------------- /Database/5. Procedures/Folders/SaveFolder.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.SaveFolder 2 | @Folder dbo.FolderTableType readonly, 3 | @WithHidReculc bit = 0 4 | as 5 | begin 6 | set nocount on; 7 | 8 | begin -- variable declaration 9 | 10 | declare 11 | @ParamId int , 12 | @ParentId int , 13 | @UserId int , 14 | @ParentHid hierarchyid , 15 | @ParentHidStr varchar(1000) , 16 | @StartTranCount int , 17 | @OldParentId int , 18 | @OldParentHid hierarchyid , 19 | @OldHid hierarchyid , 20 | @NewHid hierarchyid ; 21 | 22 | 23 | declare @FolderIds table ( 24 | InsertedId int not null, 25 | OldParentId int null, 26 | OldParentHid hierarchyid null, 27 | OldHid hierarchyid null, 28 | NewHid hierarchyid null 29 | ) 30 | 31 | end; 32 | 33 | 34 | begin try 35 | set @StartTranCount = @@trancount; 36 | if @StartTranCount = 0 37 | begin 38 | set transaction isolation level serializable; 39 | begin transaction; 40 | end; 41 | 42 | begin -- init variables and lock parent for update 43 | 44 | select @ParamId = Id, @ParentId = ParentId from @Folder; 45 | 46 | select 47 | @UserId = UserId, 48 | @ParentHid = Hid , 49 | @ParentHidStr = cast(Hid as varchar(1000)) 50 | from 51 | dbo.Folders 52 | where 53 | Id = @ParentId; 54 | end; 55 | 56 | if @WithHidReculc = 1 57 | begin -- merge calculated hierarchical data with existing folders 58 | 59 | merge into dbo.Folders as target 60 | using 61 | ( 62 | select 63 | Hid = cast(concat(@ParentHidStr, -1, '/') as varchar(1000)), 64 | Id , 65 | ParentId , 66 | [Name] 67 | from 68 | @Folder 69 | ) 70 | as source on source.Id = target.Id 71 | 72 | when matched and target.UserId = @UserId then 73 | update set 74 | ParentId = source.ParentId , 75 | [Name] = source.[Name] 76 | 77 | when not matched by target and source.Id = 0 then 78 | insert ( 79 | UserId , 80 | Hid , 81 | ParentId , 82 | Name ) 83 | values ( 84 | @UserId , 85 | source.Hid , 86 | source.ParentId , 87 | source.Name ) 88 | output 89 | inserted.Id, 90 | deleted.ParentId, 91 | deleted.Hid.GetAncestor(1) 92 | into 93 | @FolderIds ( 94 | InsertedId , 95 | OldParentId , 96 | OldParentHid ); 97 | 98 | 99 | select top 1 100 | @OldParentId = OldParentId , 101 | @OldParentHid = OldParentHid 102 | from 103 | @FolderIds 104 | 105 | --exec dbo.ResetSubFolderHids @ParentId, @ParentHid, @UserId; 106 | 107 | exec dbo.RecalcSubFolderHids @UserId, @ParentId, @ParentHid, @OldParentId, @OldParentHid ; 108 | 109 | end 110 | else 111 | begin 112 | 113 | merge into dbo.Folders as target 114 | using 115 | ( 116 | select 117 | Hid = @ParentHid.GetDescendant( 118 | LAG (case when t.Id is null then f.Hid end) over(order by coalesce(t.[Name], f.[Name])), 119 | LEAD(case when t.Id is null then f.Hid end) over(order by coalesce(t.[Name], f.[Name])) 120 | ), 121 | Id = coalesce( t.Id , f.Id ), 122 | ParentId = coalesce( t.ParentId , f.ParentId ), 123 | [Name] = coalesce( t.[Name] , f.[Name] ) 124 | from 125 | (select * from dbo.Folders where ParentId = @ParentId) f 126 | full join @Folder t on t.Id = f.Id 127 | ) 128 | as source on source.Id = @ParamId and source.Id = target.Id 129 | 130 | when matched and target.UserId = @UserId then 131 | update set 132 | Hid = source.Hid , 133 | [Name] = source.[Name] , 134 | ParentId = source.ParentId 135 | 136 | when not matched by target and source.Id = 0 then 137 | insert ( 138 | UserId , 139 | Hid , 140 | ParentId , 141 | Name ) 142 | values ( 143 | @UserId , 144 | source.Hid , 145 | source.ParentId , 146 | source.Name ) 147 | output 148 | inserted.Id , 149 | deleted.Hid , 150 | inserted.Hid 151 | into 152 | @FolderIds ( 153 | InsertedId , 154 | OldHid , 155 | NewHid ); 156 | 157 | 158 | select top 1 159 | @OldHid = OldHid , 160 | @NewHid = NewHid 161 | from 162 | @FolderIds; 163 | 164 | 165 | if @OldHid <> @NewHid 166 | update dbo.Folders set 167 | Hid = Hid.GetReparentedValue(@OldHid, @NewHid) 168 | where 169 | UserId = @UserId 170 | and Hid.IsDescendantOf(@OldHid) = 1; 171 | end; 172 | 173 | 174 | if @StartTranCount = 0 commit transaction; 175 | 176 | end try 177 | begin catch 178 | if xact_state() <> 0 and @StartTranCount = 0 rollback transaction; 179 | 180 | declare @ErrorMessage nvarchar(4000) = dbo.GetErrorMessage(); 181 | raiserror (@ErrorMessage, 16, 1); 182 | return; 183 | end catch; 184 | 185 | 186 | 187 | begin -- output of the saved Folder 188 | 189 | declare @FolderId int = coalesce 190 | ( 191 | (select InsertedId from @FolderIds), 192 | (select Id from @Folder), 193 | 0 194 | ); 195 | 196 | exec dbo.GetFolderById @Id = @FolderId; 197 | end; 198 | 199 | end; 200 | -------------------------------------------------------------------------------- /Database/5. Procedures/GrandRecords/GetGrandRecordById.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetGrandRecordById 2 | @Id int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | select 8 | * 9 | from 10 | dbo.vwGrandRecords 11 | where 12 | Id = @Id; 13 | 14 | 15 | select 16 | * 17 | from 18 | dbo.vwRecordsWithTypes 19 | where 20 | GrandRecordId = @Id 21 | order by 22 | Id; 23 | 24 | 25 | select 26 | fn.* 27 | from 28 | dbo.vwChildRecords fn 29 | inner join dbo.Records r on r.Id = fn.RecordId 30 | where 31 | GrandRecordId = @Id 32 | order by 33 | fn.RecordId, 34 | fn.Id; 35 | 36 | 37 | end; 38 | -------------------------------------------------------------------------------- /Database/5. Procedures/GrandRecords/GetGrandRecords.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetGrandRecords 2 | as 3 | begin 4 | set nocount on; 5 | 6 | select 7 | * 8 | from 9 | dbo.vwGrandRecords 10 | order by 11 | Id; 12 | 13 | 14 | select 15 | * 16 | from 17 | dbo.vwRecordsWithTypes 18 | order by 19 | GrandRecordId, 20 | Id; 21 | 22 | 23 | select 24 | fn.* 25 | from 26 | dbo.vwChildRecords fn 27 | inner join dbo.Records r on r.Id = fn.RecordId 28 | order by 29 | r.GrandRecordId, 30 | fn.RecordId, 31 | fn.Id; 32 | 33 | 34 | end; 35 | -------------------------------------------------------------------------------- /Database/5. Procedures/GrandRecords/SaveGrandRecords.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.SaveGrandRecords 2 | @GrandRecords dbo.GrandRecordTableType readonly, 3 | @Records dbo.RecordTableType readonly, 4 | @ChildRecords dbo.ChildRecordTableType readonly 5 | as 6 | begin 7 | set nocount on; 8 | 9 | declare @GrandRecordIds table ( InsertedId int primary key, ParamId int unique) 10 | declare @RecordIds table ( InsertedId int primary key, ParamId int unique, [Action] nvarchar(10)) 11 | 12 | 13 | declare @StartTranCount int; 14 | 15 | begin try 16 | set @StartTranCount = @@trancount; 17 | if @StartTranCount = 0 begin transaction; 18 | 19 | 20 | begin -- save GrandRecords 21 | 22 | merge into dbo.GrandRecords as target 23 | using 24 | ( 25 | select 26 | Id , 27 | [Name] 28 | from 29 | @GrandRecords 30 | ) 31 | as source on source.Id = target.Id 32 | 33 | when matched then 34 | update set 35 | Name = source.Name 36 | 37 | when not matched by target then 38 | insert ( 39 | Name ) 40 | values ( 41 | source.Name ) 42 | 43 | output inserted.Id, source.Id 44 | into @GrandRecordIds ( InsertedId, ParamId); 45 | 46 | end; 47 | 48 | 49 | begin -- save Records 50 | 51 | merge into dbo.Records as target 52 | using 53 | ( 54 | select 55 | Id , 56 | GrandRecordId = ids.InsertedId, 57 | Name , 58 | RecordTypeId , 59 | Number , 60 | [Date] , 61 | Amount , 62 | IsActive , 63 | Comment 64 | from 65 | @Records r 66 | inner join @GrandRecordIds ids on ids.ParamId = r.GrandRecordId 67 | ) 68 | as source on source.Id = target.Id 69 | 70 | when matched then 71 | update set 72 | GrandRecordId = source.GrandRecordId, 73 | [Name] = source.[Name] , 74 | RecordTypeId = source.RecordTypeId , 75 | Number = source.Number , 76 | [Date] = source.[Date] , 77 | Amount = source.Amount , 78 | IsActive = source.IsActive , 79 | Comment = source.Comment 80 | 81 | when not matched by target then 82 | insert ( 83 | GrandRecordId , 84 | [Name] , 85 | RecordTypeId , 86 | Number , 87 | [Date] , 88 | Amount , 89 | IsActive , 90 | Comment ) 91 | values ( 92 | source.GrandRecordId, 93 | source.[Name] , 94 | source.RecordTypeId , 95 | source.Number , 96 | source.[Date] , 97 | source.Amount , 98 | source.IsActive , 99 | source.Comment ) 100 | 101 | when not matched by source and target.GrandRecordId in (select InsertedId from @GrandRecordIds) then 102 | delete 103 | 104 | output isnull(inserted.Id, deleted.Id), isnull(source.Id, deleted.Id), $action 105 | into @RecordIds (InsertedId, ParamId, [Action]); 106 | 107 | 108 | delete from @RecordIds where [Action] = 'DELETE'; 109 | end; 110 | 111 | 112 | begin -- save ChildRecords 113 | 114 | merge into dbo.ChildRecords as target 115 | using 116 | ( 117 | select 118 | Id , 119 | RecordId = ids.InsertedId, 120 | Name 121 | from 122 | @ChildRecords cr 123 | inner join @RecordIds ids on ids.ParamId = cr.RecordId 124 | ) 125 | as source on source.Id = target.Id 126 | 127 | when matched then 128 | update set 129 | RecordId = source.RecordId , 130 | [Name] = source.[Name] 131 | 132 | when not matched by target then 133 | insert ( 134 | RecordId , 135 | [Name] ) 136 | values ( 137 | source.RecordId , 138 | source.[Name] ) 139 | 140 | when not matched by source and target.RecordId in (select InsertedId from @RecordIds) then 141 | delete; 142 | 143 | end; 144 | 145 | 146 | if @StartTranCount = 0 commit transaction; 147 | 148 | end try 149 | begin catch 150 | if xact_state() <> 0 and @StartTranCount = 0 rollback transaction; 151 | 152 | declare @ErrorMessage nvarchar(4000) = dbo.GetErrorMessage(); 153 | raiserror (@ErrorMessage, 16, 1); 154 | return; 155 | end catch; 156 | 157 | 158 | 159 | begin -- output saved Grand Records and its descendants 160 | 161 | select 162 | fn.* 163 | from 164 | dbo.vwGrandRecords fn 165 | inner join @GrandRecordIds ids on ids.InsertedId = fn.Id 166 | order by 167 | Id; 168 | 169 | 170 | select 171 | fn.* 172 | from 173 | dbo.vwRecordsWithTypes fn 174 | inner join @RecordIds ids on ids.InsertedId = fn.Id 175 | order by 176 | GrandRecordId, 177 | Id; 178 | 179 | 180 | select 181 | fn.* 182 | from 183 | dbo.vwChildRecords fn 184 | inner join dbo.Records r on r.Id = fn.RecordId 185 | inner join @RecordIds ids on ids.InsertedId = r.Id 186 | order by 187 | r.GrandRecordId, 188 | fn.RecordId, 189 | fn.Id; 190 | 191 | end; 192 | 193 | end; 194 | -------------------------------------------------------------------------------- /Database/5. Procedures/Records/GetRecordById.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetRecordById 2 | @Id int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | select 8 | * 9 | from 10 | dbo.vwRecords 11 | where 12 | Id = @Id; 13 | end; 14 | -------------------------------------------------------------------------------- /Database/5. Procedures/Records/GetRecords.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetRecords 2 | as 3 | begin 4 | set nocount on; 5 | 6 | select 7 | * 8 | from 9 | dbo.vwRecords 10 | order by 11 | Id; 12 | end; 13 | -------------------------------------------------------------------------------- /Database/5. Procedures/Records/SaveRecords.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.SaveRecords 2 | @Records dbo.RecordTableType readonly 3 | as 4 | begin 5 | set nocount on; 6 | 7 | 8 | declare @RecordIds table ( InsertedId int, ParamId int) 9 | 10 | 11 | merge into dbo.Records as target 12 | using 13 | ( 14 | select 15 | Id , 16 | GrandRecordId , 17 | [Name] , 18 | RecordTypeId , 19 | Number , 20 | [Date] , 21 | Amount , 22 | IsActive , 23 | Comment 24 | from 25 | @Records 26 | ) 27 | as source on source.Id = target.Id 28 | 29 | when matched then 30 | update set 31 | GrandRecordId = source.GrandRecordId, 32 | [Name] = source.[Name] , 33 | RecordTypeId = source.RecordTypeId , 34 | Number = source.Number , 35 | [Date] = source.[Date] , 36 | Amount = source.Amount , 37 | IsActive = source.IsActive , 38 | Comment = source.Comment 39 | 40 | when not matched by target then 41 | insert ( 42 | GrandRecordId , 43 | [Name] , 44 | RecordTypeId , 45 | Number , 46 | [Date] , 47 | Amount , 48 | IsActive , 49 | Comment ) 50 | values ( 51 | source.GrandRecordId, 52 | source.[Name] , 53 | source.RecordTypeId , 54 | source.Number , 55 | source.[Date] , 56 | source.Amount , 57 | source.IsActive , 58 | source.Comment ) 59 | 60 | output inserted.Id, source.Id 61 | into @RecordIds ( InsertedId, ParamId); 62 | 63 | 64 | 65 | select 66 | fn.* 67 | from 68 | dbo.vwRecords fn 69 | inner join @RecordIds ids on ids.InsertedId = fn.Id; 70 | 71 | end; 72 | -------------------------------------------------------------------------------- /Database/5. Procedures/SqlParameters/GetDateTimeParams.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetDateTimeParams 2 | 3 | @Date date , 4 | @DateNull date , 5 | @DateNullable date , 6 | 7 | @Time time(0) , 8 | @TimeNull time(0) , 9 | @TimeNullable time(0) , 10 | 11 | @SmallDateTime smalldatetime , 12 | @SmallDateTimeNull smalldatetime , 13 | @SmallDateTimeNullable smalldatetime , 14 | 15 | @DateTime datetime , 16 | @DateTimeNull datetime , 17 | @DateTimeNullable datetime , 18 | 19 | @DateTime2 datetime2(0) , 20 | @DateTime2Null datetime2(0) , 21 | @DateTime2Nullable datetime2(0) , 22 | 23 | @DateTimeOffset datetimeoffset(0) , 24 | @DateTimeOffsetNull datetimeoffset(0) , 25 | @DateTimeOffsetNullable datetimeoffset(0) 26 | 27 | as 28 | begin 29 | set nocount on; 30 | 31 | select 32 | 33 | @Date , 34 | @DateNull , 35 | @DateNullable , 36 | 37 | @Time , 38 | @TimeNull , 39 | @TimeNullable , 40 | 41 | @SmallDateTime , 42 | @SmallDateTimeNull , 43 | @SmallDateTimeNullable , 44 | 45 | @DateTime , 46 | @DateTimeNull , 47 | @DateTimeNullable , 48 | 49 | @DateTime2 , 50 | @DateTime2Null , 51 | @DateTime2Nullable , 52 | 53 | @DateTimeOffset , 54 | @DateTimeOffsetNull , 55 | @DateTimeOffsetNullable ; 56 | 57 | end -------------------------------------------------------------------------------- /Database/5. Procedures/SqlParameters/GetFractionalNumberParams.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetFractionalNumberParams 2 | 3 | @Decimal decimal(29,0) , 4 | @DecimalNull decimal(29,0) , 5 | @DecimalNullable decimal(29,0) , 6 | 7 | @SmallMoney smallmoney , 8 | @SmallMoneyNull smallmoney , 9 | @SmallMoneyNullable smallmoney , 10 | 11 | @Money money , 12 | @MoneyNull money , 13 | @MoneyNullable money , 14 | 15 | @Real real , 16 | @RealNull real , 17 | @RealNullable real , 18 | 19 | @Float float(53) , 20 | @FloatNull float(53) , 21 | @FloatNullable float(53) 22 | as 23 | begin 24 | set nocount on; 25 | 26 | 27 | select 28 | 29 | @Decimal , 30 | @DecimalNull , 31 | @DecimalNullable , 32 | 33 | @SmallMoney , 34 | @SmallMoneyNull , 35 | @SmallMoneyNullable , 36 | 37 | @Money , 38 | @MoneyNull , 39 | @MoneyNullable , 40 | 41 | @Real , 42 | @RealNull , 43 | @RealNullable , 44 | 45 | @Float , 46 | @FloatNull , 47 | @FloatNullable ; 48 | 49 | end 50 | -------------------------------------------------------------------------------- /Database/5. Procedures/SqlParameters/GetGuidAndRowVersionParams.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetGuidAndRowVersionParams 2 | 3 | @Guid uniqueidentifier , 4 | @GuidNull uniqueidentifier , 5 | @GuidNullable uniqueidentifier , 6 | 7 | @RowVersion binary(8) , 8 | @RowVersionNull binary(8) , 9 | 10 | @RowVersionInt64 binary(8) , 11 | @RowVersionInt64Null binary(8) , 12 | @RowVersionInt64Nullable binary(8) , 13 | 14 | @RowVersionBase64 binary(8) , 15 | @RowVersionBase64Null binary(8) 16 | 17 | as 18 | begin 19 | set nocount on; 20 | 21 | declare @t table (Id int, Rv rowversion) 22 | insert into @t (Id) values (1); 23 | 24 | select 25 | 26 | @Guid , 27 | @GuidNull , 28 | @GuidNullable , 29 | 30 | @RowVersion , 31 | @RowVersionNull , 32 | 33 | @RowVersionInt64 , 34 | @RowVersionInt64Null , 35 | @RowVersionInt64Nullable , 36 | 37 | @RowVersionBase64 , 38 | @RowVersionBase64Null , 39 | 40 | (select top 1 Rv from @t) ; 41 | 42 | end -------------------------------------------------------------------------------- /Database/5. Procedures/SqlParameters/GetStringParams.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetStringParams 2 | 3 | @Char char , 4 | @CharNull char , 5 | @CharNullable char , 6 | 7 | @NChar nchar , 8 | @NCharNull nchar , 9 | @NCharNullable nchar , 10 | 11 | @Varchar varchar(8000) , 12 | @VarcharNull varchar(8000) , 13 | 14 | @NVarchar nvarchar(4000) , 15 | @NVarcharNull nvarchar(4000) , 16 | 17 | @VarcharMax varchar(max) , 18 | @VarcharMaxNull varchar(max) , 19 | 20 | @NVarcharMax nvarchar(max) , 21 | @NVarcharMaxNull nvarchar(max) 22 | as 23 | begin 24 | set nocount on; 25 | 26 | select 27 | 28 | @Char , 29 | @CharNull , 30 | @CharNullable , 31 | 32 | @NChar , 33 | @NCharNull , 34 | @NCharNullable , 35 | 36 | @Varchar , 37 | @VarcharNull , 38 | 39 | @NVarchar , 40 | @NVarcharNull , 41 | 42 | @VarcharMax , 43 | @VarcharMaxNull , 44 | 45 | @NVarcharMax , 46 | @NVarcharMaxNull ; 47 | 48 | end -------------------------------------------------------------------------------- /Database/5. Procedures/SqlParameters/GetWholeNumberParams.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetWholeNumberParams 2 | 3 | @Bit bit , 4 | @BitNull bit , 5 | @BitNullable bit , 6 | 7 | @TinyInt tinyint , 8 | @TinyIntNull tinyint , 9 | @TinyIntNullable tinyint , 10 | 11 | @SmallInt smallint , 12 | @SmallIntNull smallint , 13 | @SmallIntNullable smallint , 14 | 15 | @Int int , 16 | @IntNull int , 17 | @IntNullable int , 18 | 19 | @BigInt bigint , 20 | @BigIntNull bigint , 21 | @BigIntNullable bigint 22 | 23 | as 24 | begin 25 | set nocount on; 26 | 27 | 28 | select 29 | @Bit , 30 | @BitNull , 31 | @BitNullable , 32 | 33 | @TinyInt , 34 | @TinyIntNull , 35 | @TinyIntNullable , 36 | 37 | @SmallInt , 38 | @SmallIntNull , 39 | @SmallIntNullable , 40 | 41 | @Int , 42 | @IntNull , 43 | @IntNullable , 44 | 45 | @BigInt , 46 | @BigIntNull , 47 | @BigIntNullable ; 48 | 49 | end 50 | -------------------------------------------------------------------------------- /Database/5. Procedures/Users/DeleteUser.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.DeleteUser 2 | @UserId int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | if @UserId between 1 and 14 -- Heros can not be deleted 8 | return 1; 9 | 10 | delete from dbo.Users where Id = @UserId; 11 | 12 | if @@rowcount = 0 13 | return 2; 14 | 15 | end; 16 | -------------------------------------------------------------------------------- /Database/5. Procedures/Users/GetUserById.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetUserById 2 | @Id int 3 | as 4 | begin 5 | set nocount on; 6 | 7 | select 8 | * 9 | from 10 | dbo.vwUsers 11 | where 12 | Id = @Id; 13 | 14 | end; 15 | -------------------------------------------------------------------------------- /Database/5. Procedures/Users/GetUsers.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.GetUsers 2 | 3 | as 4 | begin 5 | set nocount on; 6 | 7 | 8 | select 9 | fn.* 10 | from 11 | dbo.vwUsers fn 12 | order by 13 | Id; 14 | 15 | end; 16 | -------------------------------------------------------------------------------- /Database/5. Procedures/Users/SaveUser.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.SaveUser 2 | @User dbo.UserTableType readonly, 3 | @RoleIds dbo.TinyIntIdTableType readonly 4 | as 5 | begin 6 | set nocount on; 7 | 8 | if (select count(*) from @User) > 1 9 | begin 10 | raiserror ('Procedure dbo.SaveUser supports saving one User at a time only.', 16, 1); 11 | return; 12 | end 13 | 14 | declare @Login varchar(20); 15 | declare @Name nvarchar(50); 16 | declare @Email varchar(50); 17 | 18 | 19 | declare @DataReplyStatus varchar(20); 20 | declare @DataReplyMessages dbo.DataReplyMessageTableType; 21 | 22 | declare @UserIds table ( InsertedId int primary key, ParamId int unique) 23 | 24 | 25 | declare @StartTranCount int; 26 | 27 | begin try 28 | set @StartTranCount = @@trancount; 29 | if @StartTranCount = 0 begin transaction; 30 | 31 | 32 | if exists -- concurrency 33 | ( 34 | select 35 | * 36 | from 37 | dbo.Users u with (tablockx, holdlock) 38 | inner join @User t on t.Id = u.Id and t.[RowVersion] <> u.[RowVersion] 39 | ) 40 | begin 41 | 42 | select DataReplyStatus = 'Concurrency'; 43 | 44 | if @StartTranCount = 0 rollback transaction; 45 | return; 46 | end 47 | 48 | 49 | 50 | begin -- validation 51 | 52 | begin -- check User.Login uniqueness 53 | select top 1 54 | @Login = u.[Login] 55 | from 56 | dbo.Users u 57 | inner join @User t on t.[Login] = u.[Login] and t.Id <> u.Id; 58 | 59 | if @Login is not null 60 | begin 61 | set @DataReplyStatus = 'Validation'; 62 | 63 | insert into @DataReplyMessages (Code, Value) 64 | select Code ='NON_UNIQUE_LOGIN', @Login; 65 | end; 66 | end; 67 | 68 | begin -- check User.Name uniqueness 69 | select top 1 70 | @Name = u.Name 71 | from 72 | dbo.Users u 73 | inner join @User t on t.Name = u.Name and t.Id <> u.Id 74 | 75 | if @Name is not null 76 | begin 77 | set @DataReplyStatus = 'Validation'; 78 | 79 | insert into @DataReplyMessages (Code, Value) 80 | select Code ='NON_UNIQUE_NAME', @Name; 81 | end; 82 | end; 83 | 84 | begin -- check User.Email uniqueness 85 | select top 1 86 | @Email = u.Email 87 | from 88 | dbo.Users u 89 | inner join @User t on t.Email = u.Email and t.Id <> u.Id 90 | 91 | if @Email is not null 92 | begin 93 | set @DataReplyStatus = 'Validation'; 94 | 95 | insert into @DataReplyMessages (Code, Value) 96 | select Code ='NON_UNIQUE_EMAIL', @Email; 97 | end; 98 | end; 99 | 100 | select DataReplyStatus = @DataReplyStatus; 101 | 102 | if @DataReplyStatus is not null 103 | begin 104 | select * from @DataReplyMessages; 105 | 106 | if @StartTranCount = 0 rollback transaction; 107 | return; 108 | end 109 | 110 | end; 111 | 112 | 113 | begin -- save Users 114 | 115 | merge into dbo.Users as target 116 | using 117 | ( 118 | select 119 | Id , 120 | [Login] , 121 | Name , 122 | Email 123 | from 124 | @User 125 | ) 126 | as source on source.Id = target.Id 127 | 128 | when matched then 129 | update set 130 | [Login] = source.[Login] , 131 | Name = source.Name , 132 | Email = source.Email 133 | 134 | when not matched by target then 135 | insert ( 136 | [Login] , 137 | Name , 138 | Email ) 139 | values ( 140 | source.[Login] , 141 | source.Name , 142 | source.Email ) 143 | 144 | output inserted.Id, source.Id 145 | into @UserIds ( InsertedId, ParamId); 146 | 147 | end; 148 | 149 | 150 | begin -- save UserRoles 151 | 152 | merge into dbo.UserRoles as target 153 | using 154 | ( 155 | select 156 | UserId = ids.InsertedId, 157 | RoleId = r.Id 158 | from 159 | @User u 160 | inner join @UserIds ids on ids.ParamId = u.Id 161 | cross join @RoleIds r 162 | ) 163 | as source on source.UserId = target.UserId and source.RoleId = target.RoleId 164 | 165 | when not matched by target then 166 | insert ( 167 | UserId , 168 | RoleId ) 169 | values ( 170 | source.UserId , 171 | source.RoleId ) 172 | 173 | when not matched by source and target.UserId in (select InsertedId from @UserIds) then 174 | delete; 175 | end; 176 | 177 | if @StartTranCount = 0 commit transaction; 178 | 179 | end try 180 | begin catch 181 | if xact_state() <> 0 and @StartTranCount = 0 rollback transaction; 182 | 183 | declare @ErrorMessage nvarchar(4000) = dbo.GetErrorMessage(); 184 | raiserror (@ErrorMessage, 16, 1); 185 | return; 186 | end catch; 187 | 188 | 189 | 190 | begin -- output saved Users 191 | 192 | select 193 | fn.* 194 | from 195 | dbo.vwUsers fn 196 | inner join @UserIds ids on ids.InsertedId = fn.Id 197 | order by 198 | Id; 199 | 200 | end; 201 | 202 | 203 | 204 | end; 205 | -------------------------------------------------------------------------------- /Database/5. Procedures/Users/SaveUsers.sql: -------------------------------------------------------------------------------- 1 | create procedure dbo.SaveUsers 2 | @Users dbo.UserTableType readonly 3 | as 4 | begin 5 | set nocount on; 6 | 7 | declare @UserIds table ( InsertedId int primary key, ParamId int unique) 8 | 9 | 10 | declare @DataReplyStatus varchar(20); 11 | declare @DataReplyMessages dbo.DataReplyMessageTableType; 12 | --create type dbo.DataReplyMessageTableType as table 13 | --( 14 | -- Code varchar(50) not null , 15 | -- [Text] nvarchar(4000) null , 16 | -- Id bigint null , 17 | -- [Value] sql_variant null 18 | --); 19 | 20 | 21 | declare @StartTranCount int; 22 | 23 | begin try 24 | set @StartTranCount = @@trancount; 25 | if @StartTranCount = 0 begin transaction; 26 | 27 | begin -- validation 28 | 29 | 30 | with cte as 31 | ( 32 | select 33 | Id = coalesce( t.Id, u.Id ), 34 | [Login] = coalesce( t.[Login], u.[Login]), 35 | LoginRn = row_number() over(partition by coalesce( t.[Login], u.[Login] ) order by case when t.Id = u.Id or u.Id is null then 0 else 1 end), 36 | Name = coalesce( t.Name, u.Name), 37 | NameRn = row_number() over(partition by coalesce(t.Name, u.Name) order by case when t.Id = u.Id or u.Id is null then 0 else 1 end), 38 | Email = coalesce( t.Email, u.Email), 39 | EmailRn = row_number() over(partition by coalesce(t.Email, u.Email) order by case when t.Id = u.Id or u.Id is null then 0 else 1 end) 40 | from 41 | dbo.Users u with (tablockx, holdlock) 42 | full join @Users t on t.Id = u.Id 43 | ) 44 | insert into @DataReplyMessages ( 45 | Code , 46 | [Text] , 47 | Id , 48 | [Value] ) 49 | 50 | select 51 | Code = 'NON_UNIQUE_LOGIN', 52 | [Text] = 'User login is not unique', 53 | Id = Id, 54 | [Value] = [Login] 55 | from 56 | cte 57 | where 58 | LoginRn > 1 and Id > 0 59 | 60 | union all 61 | 62 | select 63 | Code = 'NON_UNIQUE_NAME', 64 | [Text] = 'User name is not unique', 65 | Id = Id, 66 | [Value] = Name 67 | from 68 | cte 69 | where 70 | LoginRn > 1 and Id > 0 71 | 72 | union all 73 | 74 | select 75 | Code = 'NON_UNIQUE_EMAIL', 76 | [Text] = 'User email is not unique', 77 | Id = Id, 78 | [Value] = Email 79 | from 80 | cte 81 | where 82 | LoginRn > 1 and Id > 0 83 | 84 | 85 | if exists(select * from @DataReplyMessages) 86 | set @DataReplyStatus = 'Validation'; 87 | 88 | 89 | select DataReplyStatus = @DataReplyStatus; 90 | 91 | if @DataReplyStatus is not null 92 | begin 93 | select * from @DataReplyMessages; 94 | 95 | if @StartTranCount = 0 rollback transaction; 96 | return; 97 | end 98 | 99 | end; 100 | 101 | 102 | begin -- save Users 103 | 104 | merge into dbo.Users as target 105 | using 106 | ( 107 | select 108 | Id , 109 | [Login] , 110 | Name , 111 | Email 112 | from 113 | @Users 114 | ) 115 | as source on source.Id = target.Id 116 | 117 | when matched then 118 | update set 119 | [Login] = source.[Login] , 120 | Name = source.Name , 121 | Email = source.Email 122 | 123 | when not matched by target then 124 | insert ( 125 | [Login] , 126 | Name , 127 | Email ) 128 | values ( 129 | source.[Login] , 130 | source.Name , 131 | source.Email ) 132 | 133 | output inserted.Id, source.Id 134 | into @UserIds ( InsertedId, ParamId); 135 | 136 | end; 137 | 138 | 139 | begin -- save UserRoles 140 | 141 | merge into dbo.UserRoles as target 142 | using 143 | ( 144 | select 145 | UserId = ids.InsertedId, 146 | RoleId = r.Id 147 | from 148 | @Users u 149 | inner join @UserIds ids on ids.ParamId = u.Id 150 | cross apply dbo.SplitTinyIntIds(u.RoleIds) r 151 | ) 152 | as source on source.UserId = target.UserId and source.RoleId = target.RoleId 153 | 154 | when not matched by target then 155 | insert ( 156 | UserId , 157 | RoleId ) 158 | values ( 159 | source.UserId , 160 | source.RoleId ) 161 | 162 | when not matched by source and target.UserId in (select InsertedId from @UserIds) then 163 | delete; 164 | end; 165 | 166 | if @StartTranCount = 0 commit transaction; 167 | 168 | end try 169 | begin catch 170 | if xact_state() <> 0 and @StartTranCount = 0 rollback transaction; 171 | 172 | declare @ErrorMessage nvarchar(4000) = dbo.GetErrorMessage(); 173 | raiserror (@ErrorMessage, 16, 1); 174 | return; 175 | end catch; 176 | 177 | 178 | 179 | begin -- output saved Users 180 | 181 | select 182 | fn.* 183 | from 184 | dbo.vwUsers fn 185 | inner join @UserIds ids on ids.InsertedId = fn.Id 186 | order by 187 | Id; 188 | 189 | end; 190 | 191 | 192 | 193 | end; 194 | -------------------------------------------------------------------------------- /Database/6. Post-Deployment/1. Users.sql: -------------------------------------------------------------------------------- 1 | use [$(DatabaseName)]; 2 | GO 3 | 4 | print 'dbo.Users table update...'; 5 | GO 6 | 7 | merge into dbo.Users as target 8 | using 9 | ( 10 | values 11 | ('thorin' , 'Thorin' , 'thorin@middle.earth' ), 12 | ('balin' , 'Balin' , 'balin@middle.earth' ), 13 | ('bifur' , 'Bifur' , 'bifur@middle.earth' ), 14 | ('bofur' , 'Bofur' , 'bofur@middle.earth' ), 15 | ('bombur' , 'Bombur' , 'bombur@middle.earth' ), 16 | ('dori' , 'Dori' , 'dori@middle.earth' ), 17 | ('dwalin' , 'Dwalin' , 'dwalin@middle.earth' ), 18 | ('fili' , 'Fili' , 'fili@middle.earth' ), 19 | ('gloin' , 'Gloin' , 'gloin@middle.earth' ), 20 | ('kili' , 'Kili' , 'kili@middle.earth' ), 21 | ('nori' , 'Nori' , 'nori@middle.earth' ), 22 | ('oin' , 'Oin' , 'oin@middle.earth' ), 23 | ('ori' , 'Ori' , 'ori@middle.earth' ), 24 | ('bilbo' , 'Bilbo' , 'bilbo@middle.earth' ) 25 | 26 | ) as source ([Login], Name, Email) on source.[Login] = target.[Login] 27 | 28 | when matched then 29 | update set 30 | [Name] = source.[Name] , 31 | Email = source.Email 32 | 33 | when not matched by target then 34 | insert ( 35 | [Login] , 36 | [Name] , 37 | Email ) 38 | values ( 39 | source.[Login], 40 | source.[Name] , 41 | source.Email) 42 | 43 | when not matched by source then 44 | delete; 45 | 46 | GO 47 | 48 | print 'OK'; 49 | GO -------------------------------------------------------------------------------- /Database/6. Post-Deployment/2. Roles.sql: -------------------------------------------------------------------------------- 1 | use [$(DatabaseName)]; 2 | GO 3 | 4 | set nocount on; 5 | 6 | print 'Enum Table synchronisation: Roles'; 7 | 8 | 9 | merge into dbo.Roles as target 10 | using ( 11 | values 12 | 13 | -- Id , Code , Name 14 | 15 | ( 1 , 'Armorer' , 'Armorer' ), 16 | ( 2 , 'Blacksmith' , 'Blacksmith' ), 17 | ( 3 , 'Bladesmith' , 'Bladesmith' ), 18 | ( 4 , 'Joiner' , 'Joiner' ), 19 | ( 5 , 'Cooper' , 'Cooper' ), 20 | ( 6 , 'Dyer' , 'Dyer' ), 21 | ( 7 , 'Furrier' , 'Furrier' ), 22 | ( 8 , 'Goldsmith' , 'Goldsmith' ), 23 | ( 9 , 'Gunsmith' , 'Gunsmith' ), 24 | ( 10 , 'Hatter' , 'Hatter' ), 25 | ( 11 , 'Locksmith' , 'Locksmith' ), 26 | ( 12 , 'Nailsmith' , 'Nailsmith' ), 27 | ( 13 , 'Potter' , 'Potter' ), 28 | ( 14 , 'Ropemaker' , 'Ropemaker' ), 29 | ( 15 , 'Saddler' , 'Saddler' ), 30 | ( 16 , 'Shoemaker' , 'Shoemaker' ), 31 | ( 17 , 'Stonemason' , 'Stonemason' ), 32 | ( 18 , 'Tailor' , 'Tailor' ), 33 | ( 19 , 'Tanner' , 'Tanner' ), 34 | ( 20 , 'Weaver' , 'Weaver' ), 35 | ( 21 , 'Wheelwright' , 'Wheelwright' ) 36 | 37 | ) as source (Id, Code, Name) on target.Code = source.Code 38 | 39 | when matched then 40 | update set 41 | Id = source.Id , 42 | Name = source.Name 43 | 44 | when not matched by target then 45 | insert (Id, Code, Name) 46 | values (Id, Code, Name) 47 | 48 | when not matched by source then 49 | delete; 50 | 51 | 52 | print 'OK'; 53 | GO -------------------------------------------------------------------------------- /Database/6. Post-Deployment/3. UserRoles.sql: -------------------------------------------------------------------------------- 1 | use [$(DatabaseName)]; 2 | GO 3 | 4 | print 'dbo.UserRoles table update...'; 5 | GO 6 | 7 | insert into dbo.UserRoles 8 | select 9 | UserId = u.Id, 10 | RoleId = r.Id 11 | from 12 | (values 13 | ('thorin' , 'Armorer' ), ('thorin', 'Joiner' ), ('thorin', 'Goldsmith' ), 14 | ('balin' , 'Blacksmith' ), ('balin' , 'Cooper' ), ('balin' , 'Gunsmith' ), 15 | ('bifur' , 'Bladesmith' ), ('bifur' , 'Dyer' ), ('bifur' , 'Hatter' ), 16 | ('bofur' , 'Joiner' ), ('bofur' , 'Furrier' ), ('bofur' , 'Locksmith' ), 17 | ('bombur' , 'Cooper' ), ('bombur', 'Goldsmith' ), ('bombur', 'Nailsmith' ), 18 | ('dori' , 'Dyer' ), ('dori' , 'Gunsmith' ), ('dori' , 'Potter' ), 19 | ('dwalin' , 'Furrier' ), ('dwalin', 'Hatter' ), ('dwalin', 'Ropemaker' ), 20 | ('fili' , 'Goldsmith' ), ('fili' , 'Locksmith' ), ('fili' , 'Saddler' ), 21 | ('gloin' , 'Gunsmith' ), ('gloin' , 'Nailsmith' ), ('gloin' , 'Shoemaker' ), 22 | ('kili' , 'Hatter' ), ('kili' , 'Potter' ), ('kili' , 'Stonemason' ), 23 | ('nori' , 'Locksmith' ), ('nori' , 'Ropemaker' ), ('nori' , 'Tailor' ), 24 | ('oin' , 'Nailsmith' ), ('oin' , 'Saddler' ), ('oin' , 'Tanner' ), 25 | ('ori' , 'Potter' ), ('ori' , 'Shoemaker' ), ('ori' , 'Weaver' ), 26 | ('bilbo' , 'Ropemaker' ), ('bilbo' , 'Stonemason' ), ('bilbo' , 'Wheelwright' ) 27 | ) t (UserLogin, RoleCode) 28 | inner join dbo.Users u on u.[Login] = t.UserLogin 29 | inner join dbo.Roles r on r.Code = t.RoleCode 30 | left join dbo.UserRoles ur on ur.UserId = u.Id and ur.RoleId = r.Id 31 | where 32 | ur.UserId is null; 33 | 34 | GO 35 | 36 | print 'OK'; 37 | GO -------------------------------------------------------------------------------- /Database/6. Post-Deployment/4. RecordTypes.sql: -------------------------------------------------------------------------------- 1 | use [$(DatabaseName)]; 2 | GO 3 | 4 | set nocount on; 5 | 6 | print 'Enum Table synchronisation: RecordTypes'; 7 | 8 | 9 | merge into dbo.RecordTypes as target 10 | using ( 11 | values 12 | 13 | -- Id , Code , Name 14 | ( 1 , 'Type_1' , N'Type One' ), 15 | ( 2 , 'Type_2' , N'Type Two' ), 16 | ( 3 , 'Type_3' , N'Type Three' ), 17 | ( 4 , 'Type_4' , N'Type Four' ), 18 | ( 5 , 'Type_5' , N'Type Five' ) 19 | 20 | ) as source (Id, Code, Name) on target.Code = source.Code 21 | 22 | when matched then 23 | update set 24 | Id = source.Id , 25 | Name = source.Name 26 | 27 | when not matched by target then 28 | insert (Id, Code, Name) 29 | values (Id, Code, Name) 30 | 31 | when not matched by source then 32 | delete; 33 | 34 | 35 | print 'OK'; 36 | GO -------------------------------------------------------------------------------- /Database/6. Post-Deployment/5. Records.sql: -------------------------------------------------------------------------------- 1 | use [$(DatabaseName)]; 2 | GO 3 | 4 | print 'Records generation...'; 5 | GO 6 | 7 | -- data generation for dbo.GrandRecords table 8 | 9 | insert into GrandRecords ([Name]) 10 | select l 11 | from (values 12 | ('(A)'),('(B)'),('(C)'),('(D)'),('(E)'),('(F)'),('(G)'),('(H)'),('(I)'),('(J)'),('(K)'),('(L)'),('(M)'), 13 | ('(N)'),('(O)'),('(P)'),('(Q)'),('(R)'),('(S)'),('(T)'),('(U)'),('(V)'),('(W)'),('(X)'),('(Y)'),('(Z)') 14 | ) t(l) 15 | where 16 | t.l not in (select [Name] from GrandRecords); 17 | 18 | 19 | -- data generation for dbo.Records table 20 | 21 | with E as 22 | ( 23 | select [Name] from (values 24 | ('A'),('B'),('C'),('D'),('E'),('F'),('G'),('H'),('I'),('J'),('K'),('L'),('M'), 25 | ('N'),('O'),('P'),('Q'),('R'),('S'),('T'),('U'),('V'),('W'),('X'),('Y'),('Z') 26 | ) t([Name]) 27 | ) 28 | insert into dbo.Records ( 29 | GrandRecordId , 30 | [Name] , 31 | RecordTypeId , 32 | Number , 33 | [Date] , 34 | Amount , 35 | IsActive , 36 | Comment ) 37 | select 38 | GrandRecordId = G.Id, 39 | [Name] = E.[Name], 40 | RecordTypeId /* tinyint */ = case when abs(checksum(newid())) % 10 = 0 then null else abs(checksum(newid())) % 5 + 1 end, 41 | Number /* smallint */ = case when abs(checksum(newid())) % 10 = 0 then null else abs(checksum(newid())) % 1000 end, 42 | [Date] /* datetime2(0) */ = case when abs(checksum(newid())) % 15 = 0 then null else dateadd(day, -1 * (abs(checksum(newid())) % 365), getdate()) end, 43 | Amount /* decimal(19,2) */ = case when abs(checksum(newid())) % 20 = 0 then null else abs(checksum(newid())) % 1000.05 end, 44 | IsActive /* bit */ = case when abs(checksum(newid())) % 30 = 0 then null else abs(checksum(newid())) % 2 end, 45 | Comment /* nvarchar(500) */ = case when abs(checksum(newid())) % 35 = 0 then null else concat('Comment # ', G.Id, '-', abs(checksum(newid())) % 1000) end 46 | from 47 | GrandRecords G, E 48 | where 49 | not exists (select * from Records ee where ee.GrandRecordId = G.Id and ee.[Name] = E.[Name] ); 50 | 51 | 52 | -- data generation for dbo.Records table 53 | 54 | with C as 55 | ( 56 | select [Name] from (values 57 | ('a'),('b'),('c'),('d'),('e'),('f'),('g'),('h'),('i'),('j'),('k'),('l'),('m'), 58 | ('n'),('o'),('p'),('q'),('r'),('s'),('t'),('u'),('v'),('w'),('x'),('y'),('z') 59 | ) t([Name]) 60 | ) 61 | insert into dbo.ChildRecords ( 62 | RecordId , 63 | [Name] ) 64 | select 65 | RecordId = E.Id, 66 | [Name] = C.[Name] 67 | from 68 | Records E, C 69 | where 70 | not exists (select * from ChildRecords cc where cc.RecordId = E.Id and cc.[Name] = C.[Name] ); 71 | 72 | GO 73 | 74 | 75 | GO 76 | 77 | print 'OK'; 78 | GO -------------------------------------------------------------------------------- /Database/6. Post-Deployment/6. Sequence reset.sql: -------------------------------------------------------------------------------- 1 | use [$(DatabaseName)]; 2 | GO 3 | 4 | create procedure #ResetSequentialId 5 | @TableName sysname , 6 | @SequenceName sysname , 7 | @IdName sysname = 'Id', 8 | @IdValue int = null 9 | as 10 | begin 11 | 12 | declare @MaxId int, @q nvarchar(500), @p nvarchar(500) 13 | 14 | if @IdValue is null 15 | begin 16 | 17 | set @q = concat('set @MaxIdOutput = isnull((select max(', @IdName, ') from dbo.', @TableName, '), 0) + 1;'); 18 | set @p = '@MaxIdOutput int output'; 19 | 20 | execute sp_executesql @q, @p, @MaxIdOutput = @MaxId output; 21 | end 22 | else 23 | begin 24 | 25 | set @MaxId = @IdValue; 26 | end 27 | 28 | set @q = concat('alter sequence dbo.', @SequenceName, ' restart with ', @MaxId); 29 | execute (@q); 30 | 31 | select @MaxId; 32 | 33 | end; 34 | 35 | 36 | GO 37 | 38 | -- execute #ResetSequentialId @TableName = 'Users', @SequenceName = 'UserId', @IdName = 'Id', @IdValue = 136 39 | 40 | -- @TableName , @SequenceName , @IdName , IdValue 41 | 42 | execute #ResetSequentialId 'Users' , 'UserId' ; 43 | execute #ResetSequentialId 'Folders' , 'FolderId' ; 44 | 45 | 46 | GO 47 | 48 | 49 | -------------------------------------------------------------------------------- /Database/6. Post-Deployment/PostDeployment.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Post-Deployment Script Template 3 | -------------------------------------------------------------------------------------- 4 | This file contains SQL statements that will be appended to the build script. 5 | Use SQLCMD syntax to include a file in the post-deployment script. 6 | Example: :r .\myfile.sql 7 | Use SQLCMD syntax to reference a variable in the post-deployment script. 8 | Example: :setvar TableName MyTable 9 | SELECT * FROM [$(TableName)] 10 | -------------------------------------------------------------------------------------- 11 | */ 12 | 13 | :R "1. Users.sql" 14 | 15 | :R "2. Roles.sql" 16 | 17 | :R "3. UserRoles.sql" 18 | 19 | :R "4. RecordTypes.sql" 20 | 21 | :R "5. Records.sql" 22 | 23 | :R "6. Sequence reset.sql" 24 | 25 | exec dbo.GenerateFolders; 26 | 27 | 28 | -------------------------------------------------------------------------------- /Database/Artisan.publish.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | True 5 | Artisan 6 | Artisan.sql 7 | Data Source=.\SQLEXPRESS;Integrated Security=True;Persist Security Info=False;Pooling=False;Multiple Active Result Sets=False;Connect Timeout=60;Encrypt=False;Trust Server Certificate=True;Command Timeout=0 8 | 1 9 | False 10 | True 11 | True 12 | 13 | -------------------------------------------------------------------------------- /Database/Database.sqlproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | Database 8 | 2.0 9 | 4.1 10 | {13350295-4fbd-470d-9258-aac77094309a} 11 | Microsoft.Data.Tools.Schema.Sql.Sql130DatabaseSchemaProvider 12 | Database 13 | 14 | 15 | Database 16 | Database 17 | 1033, CI 18 | BySchemaAndSchemaType 19 | True 20 | v4.6.1 21 | CS 22 | Properties 23 | False 24 | True 25 | True 26 | 27 | 28 | bin\Release\ 29 | $(MSBuildProjectName).sql 30 | False 31 | pdbonly 32 | true 33 | false 34 | true 35 | prompt 36 | 4 37 | 38 | 39 | bin\Debug\ 40 | $(MSBuildProjectName).sql 41 | false 42 | true 43 | full 44 | false 45 | true 46 | true 47 | prompt 48 | 4 49 | 50 | 51 | 11.0 52 | 53 | True 54 | 11.0 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 71502 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /Database/Utility Scripts/Generate MERGE SQL.sql: -------------------------------------------------------------------------------- 1 | declare @Schema sysname = N'dbo'; 2 | declare @TableName sysname = N'Users'; 3 | declare @ParamTable sysname = N'@Users' 4 | declare @LinkColumn sysname = N'Id' 5 | 6 | 7 | 8 | set nocount on; 9 | 10 | declare @MaxTabCount int; 11 | 12 | declare @SelectColumns varchar(8000); 13 | declare @UpdateColumns varchar(8000); 14 | declare @InsertToColumns varchar(8000); 15 | declare @InsertFromColumns varchar(8000); 16 | 17 | declare @SchemaTableName sysname = @Schema + '.' + @TableName; 18 | 19 | declare @Tab varchar(2) = N' '; 20 | declare @2Tabs varchar(3) = N' '; 21 | declare @3Tabs varchar(3) = N' '; 22 | declare @Caret nvarchar(2) = N' 23 | '; 24 | 25 | 26 | select 27 | @MaxTabCount = max(len(COLUMN_NAME) + 2) / 4 + case when (max(len(COLUMN_NAME) + 2) % 4) = 0 then 0 else 1 end 28 | FROM 29 | INFORMATION_SCHEMA.COLUMNS 30 | WHERE 31 | TABLE_SCHEMA = @Schema 32 | and TABLE_NAME = @TableName; 33 | 34 | 35 | select 36 | @SelectColumns = 37 | case 38 | when @SelectColumns is null 39 | then COLUMN_NAME + replicate ( @Tab , (@MaxTabCount - len(COLUMN_NAME) / 4 ) ) 40 | else 41 | @SelectColumns + ', ' + @Caret + @3Tabs + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount - len(COLUMN_NAME) / 4 ) ) 42 | end 43 | from 44 | INFORMATION_SCHEMA.COLUMNS 45 | where 46 | TABLE_SCHEMA = @Schema 47 | and TABLE_NAME = @TableName; 48 | 49 | 50 | select 51 | @UpdateColumns = 52 | case 53 | when @UpdateColumns is null 54 | then COLUMN_NAME + replicate ( @Tab , (@MaxTabCount - len(COLUMN_NAME) / 4 ) ) + '= source.' + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount + 2 - (len(COLUMN_NAME) + 11) / 4 ) ) 55 | else 56 | @UpdateColumns + ', ' + @Caret + @2Tabs + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount - len(COLUMN_NAME) / 4 ) ) + '= source.' + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount + 2 - (len(COLUMN_NAME) + 11) / 4 ) ) 57 | end, 58 | 59 | @InsertToColumns = 60 | case 61 | when @InsertToColumns is null 62 | then COLUMN_NAME + replicate ( @Tab , (@MaxTabCount - len(COLUMN_NAME) / 4 ) ) 63 | else 64 | @InsertToColumns + ', ' + @Caret + @2Tabs + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount - len(COLUMN_NAME) / 4 ) ) 65 | end, 66 | 67 | 68 | @InsertFromColumns = 69 | case 70 | when @InsertFromColumns is null 71 | then 'source.' + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount + 1 - (len(COLUMN_NAME) + 7) / 4 ) ) 72 | else 73 | @InsertFromColumns + ', ' + @Caret + @2Tabs + 'source.' + COLUMN_NAME + replicate ( @Tab , (@MaxTabCount + 1 - (len(COLUMN_NAME) + 7) / 4 ) ) 74 | end 75 | from 76 | INFORMATION_SCHEMA.COLUMNS 77 | where 78 | TABLE_SCHEMA = @Schema 79 | and TABLE_NAME = @TableName 80 | and COLUMN_NAME <> @LinkColumn; 81 | 82 | raiserror( 83 | ' 84 | merge into %s as target 85 | using 86 | ( 87 | select 88 | %s 89 | from 90 | %s 91 | ) 92 | as source on source.%s = target.%s 93 | ' 94 | ,0,1, @SchemaTableName, @SelectColumns, @ParamTable, @LinkColumn, @LinkColumn); 95 | 96 | 97 | raiserror( 98 | ' 99 | when matched then 100 | update set 101 | %s 102 | ' 103 | ,0,1, @UpdateColumns); 104 | 105 | raiserror( 106 | ' 107 | when not matched by target then 108 | insert ( 109 | %s) 110 | values ( 111 | %s) 112 | 113 | when not matched by source then 114 | delete 115 | 116 | output inserted.Id, source.Id 117 | into @%sIds ( InsertedId, ParamId); 118 | ' 119 | ,0,1, @InsertToColumns, @InsertFromColumns, @TableName); 120 | -------------------------------------------------------------------------------- /Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lobodava/artisan-orm/77db37ce13bd1c3bf2f08047de5f418363d7f353/Logo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Artisan.Orm Logo](https://raw.githubusercontent.com/lobodava/artisan-orm/master/Logo.png)](http://www.nuget.org/packages/Artisan.ORM) Artisan.ORM 2 | 3 | ADO.NET Micro-ORM to SQL Server, implemented as .NET Standard 2.1 (since version 3.5.x) or a .Net Core 6.0 library (since version 3.0.0). 4 | Use version 2.x.x, which was built with Net Standard 2.0, if you want to utilise this library with the .Net Framework or a previous version of .NET Core. 5 | 6 | ## ADO.NET Micro-ORM to SQL Server. 7 | 8 | First there was a desire to save a graph of objects for one access to the database: 9 | * one command on the client, 10 | * one request to the application server, 11 | * one access to the database. 12 | 13 | Thus the method of [How to Save Object Graph in Master-Detail Relationship with One Stored Procedure](https://www.codeproject.com/Articles/1153556/How-to-Save-Object-Graph-in-Master-Detail-Relation) was found. 14 | 15 | Then there was a desire of more control over Object-Relational Mapping, better performance and ADO.NET code reduction. 16 | 17 | Thus a set of extensions to ADO.NET methods turned into a separate project. Here is a story about [Artisan.ORM or How To Reinvent the Wheel](https://www.codeproject.com/articles/1155836/artisan-orm-or-how-to-reinvent-the-wheel)! 18 | 19 | Finally the *object graph saving method* required a new approach to transmitting of more details about exceptional cases. [Artisan Way of Data Reply](https://www.codeproject.com/Articles/1181182/Artisan-Way-of-Data-Reply) became such an answer. 20 | 21 | ## What to read for better understanding 22 | 23 | Full information about Artisan.ORM is available in [documentation Wiki](https://github.com/lobodava/artisan-orm/wiki). 24 | 25 | The most interesting articles from Wiki are: 26 | 27 | * [The Sample](https://github.com/lobodava/artisan-orm/wiki/The-Sample) 28 | * [Getting Started](https://github.com/lobodava/artisan-orm/wiki/Getting-started) 29 | * [Read Methods Understanding](https://github.com/lobodava/artisan-orm/wiki/Read-Methods-Understanding) 30 | * [Mappers](https://github.com/lobodava/artisan-orm/wiki/Mappers) 31 | * [cmd.AddTableParam](https://github.com/lobodava/artisan-orm/wiki/cmd.AddTableParam) 32 | * [Code Generation](https://github.com/lobodava/artisan-orm/wiki/Code-Generation) 33 | 34 | 35 | ## Some propositions, statements and additional information 36 | 37 | Artisan.ORM was created to meet the following requirements: 38 | * interactions with database should mostly be made through *stored procedures*; 39 | * all calls to database should be encapsulated into *repository methods*; 40 | * a *repository method* should be able to read or save a *complex object graph* with one *stored procedure*; 41 | * it should work with the highest possible performance, even at the expense of the convenience and development time. 42 | 43 | To achieve these goals Artisan.ORM uses: 44 | * the `SqlDataReader` as the fastest method of data reading; 45 | * a bunch of its own extensions to ADO.NET [SqlCommand](https://github.com/lobodava/artisan-orm/wiki/SqlCommand-Extensions) and [SqlDataReader](https://github.com/lobodava/artisan-orm/wiki/SqlDataReader-extentions) methods, both synchronous and asynchronous; 46 | * strictly structured static [Mappers](https://github.com/lobodava/artisan-orm/wiki/Mappers); 47 | * [user-defined table types](https://github.com/lobodava/artisan-orm/wiki/User-Defined-Table-Types) as a mean of object saving; 48 | * [unique negative identities](https://github.com/lobodava/artisan-orm/blob/master/Artisan.Orm/NegativeIdentity.cs) as a flag of new entities; 49 | * a [special approach](https://github.com/lobodava/artisan-orm/wiki/Negative-identities-and-object-graph-saving) to writing stored procedures for object reading and saving. 50 | 51 | Artisan.ORM is available as [NuGet Package](http://www.nuget.org/packages/Artisan.ORM). 52 | 53 | More examples of the Artisan.ORM usage are available in the [Tests](https://github.com/lobodava/artisan-orm/tree/master/Tests) and [Database](https://github.com/lobodava/artisan-orm/tree/master/Database) projects. 54 | -------------------------------------------------------------------------------- /Tests/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace Tests 4 | { 5 | public class AppSettings 6 | { 7 | public AppSettings() 8 | { 9 | var configuration = new ConfigurationBuilder() 10 | .SetBasePath(AppContext.BaseDirectory) 11 | .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) 12 | .Build(); 13 | 14 | configuration.Bind(this); 15 | } 16 | 17 | public ConnectionStrings ConnectionStrings { get; set; } 18 | } 19 | 20 | public class ConnectionStrings 21 | { 22 | public string DatabaseConnection { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tests/DAL/ByteArrayConverter.cs: -------------------------------------------------------------------------------- 1 | // http://stackoverflow.com/questions/15226921/how-to-serialize-byte-as-simple-json-array-and-not-as-base64-in-json-net 2 | 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Tests.DAL 7 | { 8 | public class ByteArrayConverter : JsonConverter 9 | { 10 | 11 | public override void Write(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) 12 | { 13 | if (value == null) 14 | { 15 | writer.WriteNullValue(); 16 | return; 17 | } 18 | 19 | byte[] data = (byte[])value; 20 | 21 | // Compose an array. 22 | writer.WriteStartArray(); 23 | 24 | for (var i = 0; i < data.Length; i++) 25 | { 26 | JsonSerializer.Serialize(writer, data[i], options); 27 | } 28 | 29 | writer.WriteEndArray(); 30 | } 31 | 32 | public override byte[] Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 33 | { 34 | 35 | if (reader.TokenType != JsonTokenType.StartArray) 36 | { 37 | throw new JsonException(); 38 | } 39 | reader.Read(); 40 | 41 | var elements = new Stack(); 42 | 43 | while (reader.TokenType != JsonTokenType.EndArray) 44 | { 45 | elements.Push(JsonSerializer.Deserialize(ref reader, options)!); 46 | 47 | reader.Read(); 48 | } 49 | 50 | return elements.ToArray(); 51 | 52 | 53 | //if (reader.TokenType == JsonToken.StartArray) 54 | //{ 55 | // var byteList = new List(); 56 | 57 | // while (reader.Read()) 58 | // { 59 | // switch (reader.TokenType) 60 | // { 61 | // case JsonToken.Integer: 62 | // byteList.Add(Convert.ToByte(reader.Value)); 63 | // break; 64 | // case JsonToken.EndArray: 65 | // return byteList.ToArray(); 66 | // case JsonToken.Comment: 67 | // // skip 68 | // break; 69 | // default: 70 | // throw new Exception( 71 | // $"Unexpected token when reading bytes: {reader.TokenType}"); 72 | // } 73 | // } 74 | 75 | // throw new Exception("Unexpected end when reading bytes."); 76 | //} 77 | //else 78 | //{ 79 | // throw new Exception( 80 | // "Unexpected token parsing binary. " + $"Expected StartArray, got {reader.TokenType}."); 81 | //} 82 | } 83 | 84 | //public override bool CanConvert(Type objectType) 85 | //{ 86 | // return objectType == typeof(byte[]); 87 | //} 88 | } 89 | } -------------------------------------------------------------------------------- /Tests/DAL/Folders/Models/Folder.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Text.Json.Serialization; 3 | using Artisan.Orm; 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace Tests.DAL.Folders.Models; 7 | 8 | public class Folder: INode 9 | { 10 | 11 | public int Id { get; set; } 12 | 13 | public int? ParentId { get; set; } 14 | 15 | [JsonIgnore] 16 | public Folder Parent { get; set; } 17 | 18 | public string Name { get; set; } 19 | 20 | public short Level { get; set; } 21 | 22 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 23 | public string Path { get; set; } 24 | 25 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 26 | public string HidCode { get; set; } 27 | 28 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 29 | public string HidPath { get; set; } 30 | 31 | [JsonIgnore] 32 | public IList Children { get; set; } 33 | 34 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 35 | public IList SubFolders 36 | { 37 | get { return Children; } 38 | set { Children = value; } 39 | } 40 | } 41 | 42 | 43 | [MapperFor(typeof(Folder), RequiredMethod.All)] // https://github.com/lobodava/artisan-orm/wiki/Mappers 44 | public static class FolderMapper 45 | { 46 | public static Folder CreateObject(SqlDataReader dr) 47 | { 48 | var i = 0; 49 | 50 | return new Folder 51 | { 52 | Id = dr.GetInt32 (i) , 53 | ParentId = dr.GetInt32Nullable (++i) , 54 | Name = dr.GetString (++i) , 55 | Level = dr.GetInt16 (++i) , 56 | 57 | HidCode = ++i < dr.FieldCount ? dr.GetStringNullable(i) : null, 58 | HidPath = ++i < dr.FieldCount ? dr.GetStringNullable(i) : null, 59 | Path = ++i < dr.FieldCount ? dr.GetStringNullable(i) : null, 60 | 61 | }; 62 | } 63 | 64 | public static ObjectRow CreateObjectRow(SqlDataReader dr) 65 | { 66 | var i = 0; 67 | 68 | return new ObjectRow(6) // https://github.com/lobodava/artisan-orm/wiki/What-is-ObjectRow%3F 69 | { 70 | /* Id 0 */ dr.GetInt32 (i) , 71 | /* ParentId 1 */ dr.GetInt32Nullable (++i) , 72 | /* Name 2 */ dr.GetString (++i) , 73 | /* Level 3 */ dr.GetInt16 (++i) , 74 | 75 | /* HidCode 4 */ ++i < dr.FieldCount ? dr.GetStringNullable(i) : null, 76 | /* HidPath 5 */ ++i < dr.FieldCount ? dr.GetStringNullable(i) : null 77 | }; 78 | } 79 | 80 | public static DataTable CreateDataTable() 81 | { 82 | return new DataTable("FolderTableType") 83 | 84 | .AddColumn( "Id" ) 85 | .AddColumn( "ParentId" ) 86 | .AddColumn( "Name" ); 87 | } 88 | 89 | public static object[] CreateDataRow(Folder obj) 90 | { 91 | return new object[] 92 | { 93 | obj.Id , 94 | obj.ParentId , 95 | obj.Name 96 | }; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /Tests/DAL/Folders/Repository.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | using Tests.DAL.Folders.Models; 3 | // https://github.com/lobodava/artisan-orm 4 | 5 | namespace Tests.DAL.Folders; 6 | 7 | public class Repository: RepositoryBase 8 | { 9 | public Repository(string connectionString) : base(connectionString) { } 10 | 11 | 12 | public Folder GetFolderById(int id) 13 | { 14 | return ReadTo("dbo.GetFolderById", cmd => cmd.AddIntParam("@Id", id)); 15 | } 16 | 17 | public Folder SaveFolder(Folder folder, bool withHidReorder = false) 18 | { 19 | return ReadTo("dbo.SaveFolder", cmd => 20 | { 21 | cmd.AddTableRowParam("@Folder", folder); 22 | cmd.AddBitParam("@WithHidReculc", withHidReorder); 23 | }); 24 | } 25 | 26 | public void DeleteFolder(int folderId, bool withHidReorder = false) 27 | { 28 | Execute("dbo.DeleteFolder", cmd => 29 | { 30 | cmd.AddIntParam("@FolderId", folderId); 31 | cmd.AddBitParam("@WithHidReculc", withHidReorder); 32 | }); 33 | } 34 | 35 | public Folder GetUserRootFolder(int userId) 36 | { 37 | return ReadTo("dbo.GetUserRootFolder", cmd => cmd.AddIntParam("@UserId", userId)); 38 | } 39 | 40 | public IList GetFolderWithSubFolders(int folderId) 41 | { 42 | return ReadToList("dbo.GetFolderWithSubFolders", cmd => cmd.AddIntParam("@FolderId", folderId)); 43 | } 44 | 45 | public ObjectRows GetFolderWithSubFoldersAsObjectRows(int folderId) 46 | { 47 | return ReadToObjectRows("dbo.GetFolderWithSubFolders", cmd => cmd.AddIntParam("@FolderId", folderId)); 48 | } 49 | 50 | public IList GetImmediateSubFolders(int folderId) 51 | { 52 | return ReadToList("dbo.GetImmediateSubFolders", cmd => cmd.AddIntParam("@FolderId", folderId)); 53 | } 54 | 55 | public IList GetFolderWithParents(int folderId) 56 | { 57 | return ReadToList("dbo.GetFolderWithParents", cmd => cmd.AddIntParam("@FolderId", folderId)); 58 | } 59 | 60 | public Folder GetNextSiblingFolder(int folderId) 61 | { 62 | return ReadTo("dbo.GetNextSiblingFolder", cmd => cmd.AddIntParam("@FolderId", folderId)); 63 | } 64 | 65 | public Folder GetPreviousSiblingFolder(int folderId) 66 | { 67 | return ReadTo("dbo.GetPreviousSiblingFolder", cmd => cmd.AddIntParam("@FolderId", folderId)); 68 | } 69 | 70 | public Folder GetFolderTree(int folderId) 71 | { 72 | return ReadToTree("dbo.GetFolderWithSubFolders", cmd => cmd.AddIntParam("@FolderId", folderId), hierarchicallySorted: true); 73 | } 74 | 75 | public Folder FindFoldersWithParentTree(int userId, short level, string folderName) 76 | { 77 | return ReadToTree("dbo.FindFoldersWithParents", cmd => 78 | { 79 | cmd.AddIntParam("@UserId", userId); 80 | cmd.AddSmallIntParam("@Level", level); 81 | cmd.AddNVarcharParam("@FolderName", 50, folderName); 82 | }); 83 | 84 | // Alternative 85 | 86 | //var folders = ReadToList("dbo.FindFoldersWithParents", cmd => 87 | //{ 88 | // cmd.AddIntParam("@UserId", userId); 89 | // cmd.AddSmallIntParam("@Level", level); 90 | // cmd.AddNVarcharParam("@FolderName", 50, folderName); 91 | //}); 92 | 93 | //return ConvertHierarchicallySortedFolderListToTrees(folders).FirstOrDefault(); 94 | } 95 | 96 | 97 | #region [ ListToTrees methods ] 98 | 99 | public static IList ConvertHierarchicallySortedFolderListToTrees(IEnumerable folders) 100 | { 101 | var parentStack = new Stack(); 102 | var parent = default(Folder); 103 | var prevNode = default(Folder); 104 | var rootNodes = new List(); 105 | 106 | foreach (var folder in folders) 107 | { 108 | if (parent == null || folder.ParentId == null) 109 | { 110 | rootNodes.Add(folder); 111 | 112 | parent = folder; 113 | } 114 | else if (folder.ParentId == parent.Id) 115 | { 116 | parent.SubFolders ??= new List(); 117 | 118 | parent.SubFolders.Add(folder); 119 | } 120 | else if (folder.ParentId == prevNode.Id) 121 | { 122 | parentStack.Push(parent); 123 | 124 | parent = prevNode; 125 | 126 | parent.SubFolders ??= new List(); 127 | 128 | parent.SubFolders.Add(folder); 129 | } 130 | else 131 | { 132 | var parentFound = false; 133 | 134 | while(parentStack.Count > 0 && parentFound == false) 135 | { 136 | parent = parentStack.Pop(); 137 | 138 | if (folder.ParentId != null && folder.ParentId.Value == parent.Id) 139 | { 140 | parent.SubFolders.Add(folder); 141 | parentFound = true; 142 | } 143 | } 144 | 145 | if (parentFound == false) 146 | { 147 | rootNodes.Add(folder); 148 | 149 | parent = folder; 150 | } 151 | } 152 | 153 | prevNode = folder; 154 | } 155 | 156 | return rootNodes; 157 | } 158 | 159 | 160 | public static IList ConvertHierarchicallyUnsortedFolderListToTrees(IEnumerable folders) 161 | { 162 | var dictionary = folders.ToDictionary(n => n.Id, n => n); 163 | var rootFolders = new List(); 164 | 165 | foreach (var folder in dictionary.Select(item => item.Value)) 166 | { 167 | if (folder.ParentId.HasValue && dictionary.TryGetValue(folder.ParentId.Value, out Folder parent)) 168 | { 169 | parent.SubFolders ??= new List(); 170 | 171 | parent.SubFolders.Add(folder); 172 | } 173 | else 174 | { 175 | rootFolders.Add(folder); 176 | } 177 | } 178 | 179 | return rootFolders; 180 | } 181 | 182 | #endregion 183 | 184 | } 185 | -------------------------------------------------------------------------------- /Tests/DAL/GrandRecords/Models/ChildRecord.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Text.Json.Serialization; 3 | using Artisan.Orm; 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace Tests.DAL.GrandRecords.Models; 7 | 8 | public class ChildRecord 9 | { 10 | public int Id { get; set; } 11 | 12 | public int RecordId { get; set; } 13 | 14 | [JsonIgnore] 15 | public Record Record { get; set; } 16 | 17 | public string Name { get; set; } 18 | } 19 | 20 | 21 | [MapperFor(typeof(ChildRecord), RequiredMethod.AllMain)] 22 | public static class ChildRecordMapper 23 | { 24 | public static ChildRecord CreateObject(SqlDataReader dr) 25 | { 26 | var i = 0; 27 | 28 | return new ChildRecord 29 | { 30 | Id = dr.GetInt32(i) , 31 | RecordId = dr.GetInt32(++i) , 32 | Name = dr.GetString(++i) , 33 | }; 34 | } 35 | 36 | public static DataTable CreateDataTable() 37 | { 38 | var table = new DataTable("ChildRecordTableType"); 39 | 40 | table.Columns.Add( "Id" , typeof(int)); 41 | table.Columns.Add( "RecordId" , typeof(int)); 42 | table.Columns.Add( "Name" , typeof(string)); 43 | 44 | return table; 45 | } 46 | 47 | public static object[] CreateDataRow(ChildRecord obj) 48 | { 49 | if (obj.Id == 0) 50 | obj.Id = Int32NegativeIdentity.Next; 51 | 52 | if (obj.RecordId == 0 && obj.Record != null) 53 | obj.RecordId = obj.Record.Id; 54 | 55 | 56 | return new object[] 57 | { 58 | obj.Id , 59 | obj.RecordId , 60 | obj.Name 61 | }; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /Tests/DAL/GrandRecords/Models/GrandRecord.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Artisan.Orm; 3 | using Microsoft.Data.SqlClient; 4 | 5 | namespace Tests.DAL.GrandRecords.Models; 6 | 7 | public class GrandRecord 8 | { 9 | public int Id { get; set; } 10 | 11 | public string Name { get; set; } 12 | 13 | public IList Records { get; set; } 14 | } 15 | 16 | 17 | [MapperFor(typeof(GrandRecord), RequiredMethod.AllMain)] 18 | public static class GrandRecordMapper 19 | { 20 | 21 | public static GrandRecord CreateObject(SqlDataReader dr) 22 | { 23 | int i = 0; 24 | 25 | return new GrandRecord 26 | { 27 | Id = dr.GetInt32(i) , 28 | Name = dr.GetString(++i) , 29 | 30 | Records = new List() 31 | }; 32 | } 33 | 34 | public static DataTable CreateDataTable() 35 | { 36 | var table = new DataTable("GrandRecordTableType"); 37 | 38 | table.Columns.Add( "Id" , typeof(int)); 39 | table.Columns.Add( "Name" , typeof(string)); 40 | 41 | return table; 42 | } 43 | 44 | public static object[] CreateDataRow(GrandRecord obj) 45 | { 46 | if (obj.Id == 0) 47 | obj.Id = Int32NegativeIdentity.Next; 48 | 49 | foreach (var record in obj.Records) 50 | record.GrandRecordId = obj.Id; 51 | 52 | 53 | return new object[] 54 | { 55 | obj.Id , 56 | obj.Name 57 | }; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /Tests/DAL/GrandRecords/Models/Record.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Text.Json.Serialization; 3 | using Artisan.Orm; 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace Tests.DAL.GrandRecords.Models; 7 | 8 | public class Record 9 | { 10 | public int Id { get; set; } 11 | 12 | public int GrandRecordId { get; set; } 13 | 14 | [JsonIgnore] 15 | public GrandRecord GrandRecord { get; set; } 16 | 17 | public string Name { get; set; } 18 | 19 | public byte? RecordTypeId { get; set; } 20 | public RecordType RecordType { get; set; } 21 | 22 | public short? Number { get; set; } 23 | 24 | public DateTime? Date { get; set; } 25 | 26 | public decimal? Amount { get; set; } 27 | 28 | public bool? IsActive { get; set; } 29 | 30 | public string Comment { get; set; } 31 | 32 | public IList ChildRecords { get; set; } 33 | } 34 | 35 | 36 | [MapperFor(typeof(Record), RequiredMethod.All)] 37 | public static class RecordMapper 38 | { 39 | public static Record CreateObject(SqlDataReader dr) 40 | { 41 | var i = 0; 42 | 43 | return new Record 44 | { 45 | Id = dr.GetInt32(i) , 46 | GrandRecordId = dr.GetInt32(++i) , 47 | Name = dr.GetString(++i) , 48 | RecordTypeId = dr.GetByteNullable(++i) , 49 | Number = dr.GetInt16Nullable(++i) , 50 | Date = dr.GetDateTimeNullable(++i) , 51 | Amount = dr.GetDecimalNullable(++i) , 52 | IsActive = dr.GetBooleanNullable(++i) , 53 | Comment = dr.GetStringNullable(++i) , 54 | 55 | RecordType = RecordTypeMapper.CreateObject(dr, ref i), 56 | 57 | ChildRecords = new List() 58 | }; 59 | } 60 | 61 | public static ObjectRow CreateObjectRow(SqlDataReader dr) 62 | { 63 | var i = 0; 64 | 65 | return new ObjectRow(9) 66 | { 67 | /* 0 - Id = */ dr.GetInt32(i) , 68 | /* 1 - GrandRecordId = */ dr.GetInt32(++i) , 69 | /* 2 - Name = */ dr.GetString(++i) , 70 | /* 3 - RecordTypeId = */ dr.GetByteNullable(++i) , 71 | /* 4 - Number = */ dr.GetInt16Nullable(++i) , 72 | /* 5 - Date = */ dr.GetDateTimeNullable(++i) , 73 | /* 6 - Amount = */ dr.GetDecimalNullable(++i) , 74 | /* 7 - IsActive = */ dr.GetBooleanNullable(++i) , 75 | /* 8 - Comment = */ dr.GetStringNullable(++i) , 76 | }; 77 | } 78 | 79 | 80 | public static DataTable CreateDataTable() 81 | { 82 | var table = new DataTable("RecordTableType"); 83 | 84 | table.Columns.Add( "Id" , typeof(int)); 85 | table.Columns.Add( "GrandRecordId" , typeof(int)); 86 | table.Columns.Add( "Name" , typeof(string)); 87 | table.Columns.Add( "RecordTypeId" , typeof(byte)); 88 | table.Columns.Add( "Number" , typeof(short)); 89 | table.Columns.Add( "Date" , typeof( DateTime )); 90 | table.Columns.Add( "Amount" , typeof(decimal)); 91 | table.Columns.Add( "IsActive" , typeof(bool)); 92 | table.Columns.Add( "Comment" , typeof(string)); 93 | 94 | return table; 95 | } 96 | 97 | public static object[] CreateDataRow(Record obj) 98 | { 99 | if (obj.Id == 0) 100 | obj.Id = Int32NegativeIdentity.Next; 101 | 102 | if (obj.GrandRecordId == 0 && obj.GrandRecord != null) 103 | obj.GrandRecordId = obj.GrandRecord.Id; 104 | 105 | if (obj.RecordTypeId == null && obj.RecordType != null) 106 | obj.RecordTypeId = obj.RecordType.Id; 107 | 108 | foreach (var childRecord in obj.ChildRecords) 109 | childRecord.RecordId = obj.Id; 110 | 111 | 112 | return new object[] 113 | { 114 | obj.Id , 115 | obj.GrandRecordId , 116 | obj.Name , 117 | obj.RecordTypeId , 118 | obj.Number , 119 | obj.Date , 120 | obj.Amount , 121 | obj.IsActive , 122 | obj.Comment 123 | }; 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /Tests/DAL/GrandRecords/Models/RecordType.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | using Microsoft.Data.SqlClient; 3 | 4 | namespace Tests.DAL.GrandRecords.Models; 5 | 6 | public class RecordType 7 | { 8 | public byte Id { get; set; } 9 | 10 | public string Code { get; set; } 11 | 12 | public string Name { get; set; } 13 | } 14 | 15 | 16 | [MapperFor(typeof(RecordType), RequiredMethod.CreateObject)] 17 | public static class RecordTypeMapper 18 | { 19 | public static RecordType CreateObject(SqlDataReader dr) 20 | { 21 | var index = 0; 22 | return CreateObject(dr, ref index); 23 | } 24 | 25 | public static RecordType CreateObject(SqlDataReader dr, ref int index) 26 | { 27 | if (dr.IsDBNull(++index)) 28 | { 29 | index += 2; 30 | return null; 31 | } 32 | 33 | return new RecordType 34 | { 35 | Id = dr.GetByte(index) , 36 | Code = dr.GetString(++index) , 37 | Name = dr.GetString(++index) 38 | }; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /Tests/DAL/GrandRecords/Repository.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | using Microsoft.Data.SqlClient; 3 | using Tests.DAL.GrandRecords.Models; 4 | 5 | namespace Tests.DAL.GrandRecords; 6 | 7 | public class Repository: RepositoryBase 8 | { 9 | public Repository(string connectionString) : base(connectionString) { } 10 | 11 | 12 | #region [ Get ONE Grand Record (with its descendants) ] 13 | 14 | 15 | public GrandRecord GetGrandRecordById(int id) 16 | { 17 | return GetByCommand(cmd => 18 | { 19 | cmd.UseProcedure("dbo.GetGrandRecordById"); 20 | 21 | cmd.AddIntParam("@Id", id); 22 | 23 | return cmd.GetByReader(ReadGrandRecord); 24 | }); 25 | } 26 | 27 | public async Task GetGrandRecordByIdAsync(int id) 28 | { 29 | return await GetByCommandAsync(cmd => 30 | { 31 | cmd.UseProcedure("dbo.GetGrandRecordById"); 32 | 33 | cmd.AddIntParam("@Id", id); 34 | 35 | return cmd.GetByReaderAsync(ReadGrandRecord); 36 | }); 37 | } 38 | 39 | private static GrandRecord ReadGrandRecord(SqlDataReader reader) 40 | { 41 | var grandRecord = reader.ReadTo(); 42 | 43 | grandRecord.Records = reader.ReadToList(); 44 | // or reader.ReadToList(grandRecord.Records); 45 | 46 | var childRecords = reader.ReadToList(); 47 | 48 | reader.Close(); 49 | 50 | // the following code allows joining two collections for a single pass 51 | // it works only if these collections are sorted by RecordId (!) 52 | 53 | grandRecord.Records.MergeJoin( 54 | r => { r.GrandRecord = grandRecord; }, 55 | childRecords, 56 | (r, cr) => r.Id == cr.RecordId, 57 | (r, cr) => {cr.Record = r; r.ChildRecords.Add(cr);} 58 | ); 59 | 60 | // THE MergeJoin CODE ABOVE IS THE REPLACEMENT FOR THE COMMENTED CODE BELOW :) 61 | 62 | //var childRecordEnumerator = childRecords.GetEnumerator(); 63 | //var childRecord = childRecordEnumerator.MoveNext() ? childRecordEnumerator.Current : null; 64 | 65 | //foreach (var record in grandRecord.Records) 66 | //{ 67 | // record.GrandRecord = grandRecord; 68 | 69 | // while (childRecord != null && childRecord.RecordId == record.Id) 70 | // { 71 | // childRecord.Record = record; 72 | // record.ChildRecords.Add(childRecord); 73 | 74 | // childRecord = childRecordEnumerator.MoveNext() ? childRecordEnumerator.Current : null; 75 | // } 76 | 77 | // record.GrandRecord = grandRecord; 78 | //} 79 | 80 | return grandRecord; 81 | } 82 | 83 | 84 | #endregion 85 | 86 | 87 | #region [ Get MANY Grand Records (with their descendants) ] 88 | 89 | 90 | public IList GetGrandRecords() 91 | { 92 | return GetByCommand(cmd => 93 | { 94 | cmd.UseProcedure("dbo.GetGrandRecords"); 95 | 96 | return cmd.GetByReader(ReadGrandRecords); 97 | }); 98 | } 99 | 100 | public async Task> GetGrandRecordsAsync() 101 | { 102 | return await GetByCommandAsync(cmd => 103 | { 104 | cmd.UseProcedure("dbo.GetGrandRecords"); 105 | 106 | return cmd.GetByReaderAsync(ReadGrandRecords); 107 | }); 108 | } 109 | 110 | #endregion 111 | 112 | 113 | #region [ Save MANY Grand Records (with their descendants) ] 114 | 115 | 116 | public IList SaveGrandRecords(IList grandRecords) 117 | { 118 | var records = grandRecords.SelectMany(gr => gr.Records); 119 | var childRecords = records.SelectMany(r => r.ChildRecords); 120 | 121 | return GetByCommand(cmd => 122 | { 123 | cmd.UseProcedure("dbo.SaveGrandRecords"); 124 | 125 | cmd.AddTableParam("@GrandRecords", grandRecords); 126 | 127 | cmd.AddTableParam("@Records", records); 128 | 129 | cmd.AddTableParam("@ChildRecords", childRecords); 130 | 131 | return cmd.GetByReader(ReadGrandRecords); 132 | }); 133 | } 134 | 135 | public async Task> SaveGrandRecordsAsync(IList grandRecords) 136 | { 137 | var records = grandRecords.SelectMany(gr => gr.Records); 138 | var childRecords = records.SelectMany(r => r.ChildRecords); 139 | 140 | return await GetByCommandAsync(cmd => 141 | { 142 | cmd.UseProcedure("dbo.SaveGrandRecords"); 143 | 144 | cmd.AddTableParam("@GrandRecords", grandRecords); 145 | 146 | cmd.AddTableParam("@Records", records); 147 | 148 | cmd.AddTableParam("@ChildRecords", childRecords); 149 | 150 | return cmd.GetByReaderAsync(ReadGrandRecords); 151 | }); 152 | } 153 | 154 | #endregion 155 | 156 | 157 | private static IList ReadGrandRecords(SqlDataReader reader) 158 | { 159 | var grandRecords = reader.ReadToList(); 160 | 161 | var records = reader.ReadToList(); 162 | 163 | var childRecords = reader.ReadToList(); 164 | 165 | reader.Close(); 166 | 167 | // the following code allows joining two collections for a single pass 168 | // it works only if these collections are sorted by RecordId (!) 169 | 170 | grandRecords.MergeJoin( 171 | records, 172 | (gr, r) => gr.Id == r.GrandRecordId, 173 | (gr, r) => { r.GrandRecord = gr; gr.Records.Add(r); }, 174 | 175 | childRecords, 176 | (r, cr) => r.Id == cr.RecordId, 177 | (r, cr) => { cr.Record = r; r.ChildRecords.Add(cr); } 178 | ); 179 | 180 | // THE MergeJoin CODE ABOVE IS THE REPLACEMENT FOR THE COMMENTED CODE BELOW :) 181 | 182 | //var recordEnumerator = records.GetEnumerator(); 183 | //var record = recordEnumerator.MoveNext() ? recordEnumerator.Current : null; 184 | 185 | //var childRecordEnumerator = childRecords.GetEnumerator(); 186 | //var childRecord = childRecordEnumerator.MoveNext() ? childRecordEnumerator.Current : null; 187 | 188 | 189 | //foreach (var grandRecord in grandRecords) 190 | //{ 191 | // while (record != null && record.GrandRecordId == grandRecord.Id) 192 | // { 193 | // while (childRecord != null && childRecord.RecordId == record.Id) 194 | // { 195 | // childRecord.Record = record; 196 | // record.ChildRecords.Add(childRecord); 197 | 198 | // childRecord = childRecordEnumerator.MoveNext() ? childRecordEnumerator.Current : null; 199 | // } 200 | 201 | // record.GrandRecord = grandRecord; 202 | // grandRecord.Records.Add(record); 203 | 204 | // record = recordEnumerator.MoveNext() ? recordEnumerator.Current : null; 205 | // } 206 | //} 207 | 208 | return grandRecords; 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /Tests/DAL/Records/Models/Record.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Artisan.Orm; 3 | using Microsoft.Data.SqlClient; 4 | 5 | namespace Tests.DAL.Records.Models; 6 | 7 | public class Record 8 | { 9 | public int Id { get; set; } 10 | 11 | public int GrandRecordId { get; set; } 12 | 13 | public string Name { get; set; } 14 | 15 | public byte? RecordTypeId { get; set; } 16 | 17 | public short? Number { get; set; } 18 | 19 | public DateTime? Date { get; set; } 20 | 21 | public decimal? Amount { get; set; } 22 | 23 | public bool? IsActive { get; set; } 24 | 25 | public string Comment { get; set; } 26 | } 27 | 28 | 29 | [MapperFor(typeof(Record), RequiredMethod.All)] 30 | public static class RecordMapper 31 | { 32 | public static Record CreateObject(SqlDataReader dr) 33 | { 34 | var i = 0; 35 | 36 | return new Record 37 | { 38 | Id = dr.GetInt32(i) , 39 | GrandRecordId = dr.GetInt32(++i) , 40 | Name = dr.GetString(++i) , 41 | RecordTypeId = dr.GetByteNullable(++i) , 42 | Number = dr.GetInt16Nullable(++i) , 43 | Date = dr.GetDateTimeNullable(++i) , 44 | Amount = dr.GetDecimalNullable(++i) , 45 | IsActive = dr.GetBooleanNullable(++i) , 46 | Comment = dr.GetStringNullable(++i) , 47 | }; 48 | 49 | //return new Record 50 | //{ 51 | // Id = (Int32)dr.GetValue(++i) , 52 | // GrandRecordId = (Int32)dr.GetValue(++i) , 53 | // Name = (String)dr.GetValue(++i) , 54 | // RecordTypeId = dr.IsDBNull(i) ? default(Byte?) : (Byte?)dr.GetValue(++i) , 55 | // Number = dr.IsDBNull(i) ? default(Int16?) : (Int16?)dr.GetValue(++i) , 56 | // Date = dr.IsDBNull(i) ? default(DateTime?) : (DateTime?)dr.GetValue(++i) , 57 | // Amount = dr.IsDBNull(i) ? default(Decimal?) : (Decimal?)dr.GetValue(++i) , 58 | // IsActive = dr.IsDBNull(i) ? default(Boolean?) : (Boolean?)dr.GetValue(++i) , 59 | // Comment = dr.IsDBNull(i) ? null : (String)dr.GetValue(++i) 60 | //}; 61 | 62 | 63 | // cast with "as" is a fast way and a dangerous one 64 | // if cast fails "as" returns and not throw an exception 65 | 66 | //return new Record 67 | //{ 68 | // Id = (Int32)dr.GetValue(++i) , 69 | // GrandRecordId = (Int32)dr.GetValue(++i) , 70 | // Name = (String)dr.GetValue(++i) , 71 | // RecordTypeId = dr.GetValue(++i) as Byte? , 72 | // Number = dr.GetValue(++i) as Int16? , 73 | // Date = dr.GetValue(++i) as DateTime? , 74 | // Amount = dr.GetValue(++i) as Decimal? , 75 | // IsActive = dr.GetValue(++i) as Boolean? , 76 | // Comment = dr.GetValue(i) as String 77 | //}; 78 | 79 | } 80 | 81 | public static ObjectRow CreateObjectRow(SqlDataReader dr) 82 | { 83 | var i = 0; 84 | 85 | return new ObjectRow(9) 86 | { 87 | /* 0 - Id = */ dr.GetInt32(i) , 88 | /* 1 - GrandRecordId = */ dr.GetInt32(++i) , 89 | /* 2 - Name = */ dr.GetString(++i) , 90 | /* 3 - RecordTypeId = */ dr.GetByteNullable(++i) , 91 | /* 4 - Number = */ dr.GetInt16Nullable(++i) , 92 | /* 5 - Date = */ dr.GetDateTimeNullable(++i) , 93 | /* 6 - Amount = */ dr.GetDecimalNullable(++i) , 94 | /* 7 - IsActive = */ dr.GetBooleanNullable(++i) , 95 | /* 8 - Comment = */ dr.GetStringNullable(++i) , 96 | }; 97 | } 98 | 99 | 100 | public static DataTable CreateDataTable() 101 | { 102 | //var table = new DataTable("RecordTableType"); 103 | 104 | //table.Columns.Add( "Id" , typeof( Int32 )); 105 | //table.Columns.Add( "GrandRecordId" , typeof( Int32 )); 106 | //table.Columns.Add( "Name" , typeof( String )); 107 | //table.Columns.Add( "RecordTypeId" , typeof( Byte )); 108 | //table.Columns.Add( "Number" , typeof( Int16 )); 109 | //table.Columns.Add( "Date" , typeof( DateTime )); 110 | //table.Columns.Add( "Amount" , typeof( Decimal )); 111 | //table.Columns.Add( "IsActive" , typeof( Boolean )); 112 | //table.Columns.Add( "Comment" , typeof( String )); 113 | 114 | //return table; 115 | 116 | return new DataTable( "RecordTableType" ) 117 | .AddColumn ( "Id" ) 118 | .AddColumn ( "GrandRecordId" ) 119 | .AddColumn ( "Name" ) 120 | .AddColumn ( "RecordTypeId" ) 121 | .AddColumn ( "Number" ) 122 | .AddColumn( "Date" ) 123 | .AddColumn ( "Amount" ) 124 | .AddColumn ( "IsActive" ) 125 | .AddColumn ( "Comment" ); 126 | 127 | } 128 | 129 | public static object[] CreateDataRow(Record obj) 130 | { 131 | if (obj.Id == 0) 132 | obj.Id = Int32NegativeIdentity.Next; 133 | 134 | 135 | return new object[] 136 | { 137 | obj.Id , 138 | obj.GrandRecordId , 139 | obj.Name , 140 | obj.RecordTypeId , 141 | obj.Number , 142 | obj.Date , 143 | obj.Amount , 144 | obj.IsActive , 145 | obj.Comment 146 | }; 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /Tests/DAL/Records/Repository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.Data.SqlClient; 3 | using System.Threading.Tasks; 4 | using Artisan.Orm; 5 | using Tests.DAL.Records.Models; 6 | 7 | namespace Tests.DAL.Records 8 | { 9 | public class Repository: RepositoryBase 10 | { 11 | public Repository(string connectionString) : base(connectionString) { } 12 | 13 | 14 | #region [ GetRecordById ] 15 | 16 | 17 | public Record GetRecordById(int id) 18 | { 19 | return GetByCommand(cmd => 20 | { 21 | cmd.UseProcedure("dbo.GetRecordById"); 22 | 23 | cmd.AddIntParam("Id", id); 24 | 25 | return cmd.ReadTo(); 26 | }); 27 | } 28 | 29 | public Record GetRecordByIdWithAutoMapping(int id) 30 | { 31 | return GetByCommand(cmd => 32 | { 33 | cmd.UseProcedure("dbo.GetRecordById"); 34 | 35 | cmd.AddIntParam("Id", id); 36 | 37 | return cmd.ReadAs(); 38 | }); 39 | } 40 | 41 | public Record GetRecordByIdOnBaseLevel(int id) 42 | { 43 | //var sql = @"select 44 | // Id , 45 | // GrandRecordId , 46 | // Name , 47 | // RecordTypeId , 48 | // Number , 49 | // [Date] , 50 | // Amount , 51 | // IsActive , 52 | // Comment 53 | // from 54 | // dbo.Records 55 | // where 56 | // Id = @Id"; 57 | 58 | var sql = "dbo.GetRecordById"; 59 | 60 | return ReadTo(sql, new SqlParameter("Id", id)); 61 | } 62 | 63 | 64 | public async Task GetRecordByIdAsync(int id) 65 | { 66 | return await GetByCommandAsync(cmd => 67 | { 68 | cmd.UseProcedure("dbo.GetRecordById"); 69 | 70 | cmd.AddIntParam("@Id", id); 71 | 72 | return cmd.ReadToAsync(); 73 | }); 74 | } 75 | 76 | 77 | #endregion 78 | 79 | 80 | #region [ GetRecords ] 81 | 82 | 83 | public IList GetRecords() 84 | { 85 | return ReadToList("dbo.GetRecords"); 86 | 87 | //return GetByCommand(cmd => 88 | //{ 89 | // cmd.UseProcedure("dbo.GetRecords"); 90 | 91 | // return cmd.ReadToList(); 92 | //}); 93 | } 94 | 95 | public IList GetRecordsWithAutoMapping() 96 | { 97 | return GetByCommand(cmd => 98 | { 99 | cmd.UseProcedure("dbo.GetRecords"); 100 | 101 | return cmd.ReadAsList(); 102 | }); 103 | } 104 | 105 | public async Task> GetRecordsAsync() 106 | { 107 | return await GetByCommandAsync(cmd => 108 | { 109 | cmd.UseProcedure("dbo.GetRecords"); 110 | 111 | return cmd.ReadToListAsync(); 112 | }); 113 | } 114 | 115 | public async Task> GetRecordsWithAutoMappingAsync() 116 | { 117 | return await GetByCommandAsync(cmd => 118 | { 119 | cmd.UseProcedure("dbo.GetRecords"); 120 | 121 | return cmd.ReadAsListAsync(); 122 | }); 123 | } 124 | 125 | public IEnumerable GetRecordsToEnumerable() 126 | { 127 | return GetByCommand(cmd => 128 | { 129 | cmd.UseProcedure("dbo.GetRecords"); 130 | 131 | return cmd.ReadToEnumerable(); 132 | }); 133 | } 134 | 135 | public IEnumerable GetRecordsAsEnumerable() 136 | { 137 | return GetByCommand(cmd => 138 | { 139 | cmd.UseProcedure("dbo.GetRecords"); 140 | 141 | return cmd.ReadAsEnumerable(); 142 | }); 143 | } 144 | 145 | 146 | public IEnumerable GetRecordsToEnumerableOnBaseLevel() 147 | { 148 | return ReadToEnumerable("dbo.GetRecords"); 149 | } 150 | 151 | public IEnumerable GetRecordsAsEnumerableOnBaseLevel() 152 | { 153 | return ReadAsEnumerable("dbo.GetRecords"); 154 | } 155 | 156 | 157 | public IList GetRecordsOnBaseLevel() 158 | { 159 | return ReadToList("dbo.GetRecords"); 160 | } 161 | 162 | 163 | #endregion 164 | 165 | #region [ GetRecordRows ] 166 | 167 | 168 | public ObjectRows GetRecordRows() 169 | { 170 | return GetByCommand(cmd => 171 | { 172 | cmd.UseProcedure("dbo.GetRecords"); 173 | 174 | return cmd.ReadToObjectRows(); 175 | }); 176 | } 177 | 178 | public async Task GetRecordRowsAsync() 179 | { 180 | return await GetByCommandAsync(cmd => 181 | { 182 | cmd.UseProcedure("dbo.GetRecords"); 183 | 184 | return cmd.ReadToObjectRowsAsync(); 185 | }); 186 | } 187 | 188 | public ObjectRows GetRecordRowsOnBaseLevel() 189 | { 190 | return ReadToObjectRows("dbo.GetRecords"); 191 | } 192 | 193 | public ObjectRows GetRecordRowsWithHandMapping() 194 | { 195 | return GetByCommand(cmd => 196 | { 197 | cmd.UseProcedure("dbo.GetRecords"); 198 | 199 | return cmd.ReadToObjectRows(dr => new ObjectRow(9) 200 | { 201 | /* Id */ dr.GetInt32(0) , 202 | /* GrandRecordId */ dr.GetInt32(1) , 203 | /* Name */ dr.GetString(2) , 204 | /* RecordTypeId */ dr.GetByteNullable(3) , 205 | /* Number */ dr.GetInt16Nullable(4) , 206 | /* Date */ dr.GetDateTimeNullable(5) , 207 | /* Amount */ dr.GetDecimalNullable(6) , 208 | /* IsActive */ dr.GetBooleanNullable(7) , 209 | /* Comment */ dr.GetStringNullable(8) 210 | }); 211 | }); 212 | } 213 | 214 | public async Task GetRecordRowsWithHandMappingAsync() 215 | { 216 | return await GetByCommandAsync(cmd => 217 | { 218 | cmd.UseProcedure("dbo.GetRecords"); 219 | 220 | return cmd.ReadToObjectRowsAsync(dr => new ObjectRow(9) 221 | { 222 | /* Id */ dr.GetInt32(0) , 223 | /* GrandRecordId */ dr.GetInt32(1) , 224 | /* Name */ dr.GetString(2) , 225 | /* RecordTypeId */ dr.GetByteNullable(3) , 226 | /* Number */ dr.GetInt16Nullable(4) , 227 | /* Date */ dr.GetDateTimeNullable(5) , 228 | /* Amount */ dr.GetDecimalNullable(6) , 229 | /* IsActive */ dr.GetBooleanNullable(7) , 230 | /* Comment */ dr.GetStringNullable(8) 231 | }); 232 | }); 233 | } 234 | 235 | #endregion 236 | 237 | 238 | #region [ Save ONE Record ] 239 | 240 | public Record SaveRecord(Record record) 241 | { 242 | return GetByCommand(cmd => 243 | { 244 | cmd.UseProcedure("dbo.SaveRecords"); 245 | 246 | cmd.AddTableRowParam("@Records", record); 247 | 248 | return cmd.ReadTo(); 249 | }); 250 | } 251 | 252 | public async Task SaveRecordAsync(Record record) 253 | { 254 | return await GetByCommandAsync(cmd => 255 | { 256 | cmd.UseProcedure("dbo.SaveRecords"); 257 | 258 | cmd.AddTableRowParam("@Records", record); 259 | 260 | return cmd.ReadToAsync(); 261 | }); 262 | } 263 | 264 | 265 | #endregion 266 | 267 | #region [ Save MANY Records ] 268 | 269 | 270 | public IList SaveRecords(IList records) 271 | { 272 | return GetByCommand(cmd => 273 | { 274 | cmd.UseProcedure("dbo.SaveRecords"); 275 | 276 | cmd.AddTableParam("@Records", records); 277 | 278 | return cmd.ReadToList(); 279 | }); 280 | } 281 | 282 | public async Task> SaveRecordsAsync(IList records) 283 | { 284 | return await GetByCommandAsync(cmd => 285 | { 286 | cmd.UseProcedure("dbo.SaveRecords"); 287 | 288 | cmd.AddTableParam("@Records", records.ToDataTable()); 289 | 290 | return cmd.ReadToListAsync(); 291 | }); 292 | } 293 | 294 | 295 | #endregion 296 | 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /Tests/DAL/Users/Models/Role.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | using Microsoft.Data.SqlClient; 3 | 4 | namespace Tests.DAL.Users.Models; 5 | 6 | public class Role 7 | { 8 | public byte Id { get; set; } 9 | 10 | public string Code { get; set; } 11 | 12 | public string Name { get; set; } 13 | } 14 | 15 | 16 | [MapperFor(typeof(Role), RequiredMethod.CreateObject)] 17 | public static class RoleMapper 18 | { 19 | public static Role CreateObject(SqlDataReader dr) 20 | { 21 | var i = 0; 22 | 23 | return new Role 24 | { 25 | Id = dr.GetByte(i) , 26 | Code = dr.GetString(++i) , 27 | Name = dr.GetString(++i) 28 | }; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Tests/DAL/Users/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Text.Json.Serialization; 3 | using Artisan.Orm; 4 | using Microsoft.Data.SqlClient; 5 | 6 | namespace Tests.DAL.Users.Models; 7 | 8 | public class User 9 | { 10 | public int Id { get; set; } 11 | 12 | public string Login { get; set; } 13 | 14 | public string Name { get; set; } 15 | 16 | public string Email { get; set; } 17 | 18 | public string RowVersion { get; set; } 19 | 20 | 21 | [JsonConverter(typeof(ByteArrayConverter))] 22 | public byte[] RoleIds { get; set; } 23 | 24 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 25 | public IList Roles { get; set; } 26 | } 27 | 28 | 29 | [MapperFor(typeof(User), RequiredMethod.All)] 30 | public static class UserMapper 31 | { 32 | public static User CreateObject(SqlDataReader dr) 33 | { 34 | var i = 0; 35 | 36 | return new User 37 | { 38 | Id = dr.GetInt32(i) , 39 | Login = dr.GetString(++i) , 40 | Name = dr.GetString(++i) , 41 | Email = dr.GetString(++i) , 42 | RowVersion = dr.GetBase64StringFromRowVersion(++i), 43 | RoleIds = ++i < dr.FieldCount ? dr.GetByteArrayFromString(i) : null 44 | }; 45 | } 46 | 47 | public static ObjectRow CreateObjectRow(SqlDataReader dr) 48 | { 49 | var i = 0; 50 | 51 | return new ObjectRow(5) 52 | { 53 | /* 0 - Id = */ dr.GetInt32(i) , 54 | /* 1 - Login = */ dr.GetString(++i) , 55 | /* 2 - Name = */ dr.GetString(++i) , 56 | /* 3 - Email = */ dr.GetString(++i) , 57 | /* 4 - RowVersion = */ dr.GetBase64StringFromRowVersion(++i), 58 | /* 5 - RoleIds = */ dr.GetInt16ArrayFromString(++i) 59 | }; 60 | } 61 | 62 | 63 | public static DataTable CreateDataTable() 64 | { 65 | return new DataTable("UserTableType") 66 | 67 | .AddColumn< int >( "Id" ) 68 | .AddColumn< string >( "Login" ) 69 | .AddColumn< string >( "Name" ) 70 | .AddColumn< string >( "Email" ) 71 | .AddColumn< byte[] >( "RowVersion" ) 72 | .AddColumn< string >( "RoleIds" ); 73 | } 74 | 75 | public static object[] CreateDataRow(User obj) 76 | { 77 | if (obj.Id == 0) 78 | obj.Id = Int32NegativeIdentity.Next; 79 | 80 | return new object[] 81 | { 82 | obj.Id , 83 | obj.Login , 84 | obj.Name , 85 | obj.Email , 86 | Convert.FromBase64String(obj.RowVersion), 87 | obj.RoleIds == null ? null : string.Join(",", obj.RoleIds) 88 | }; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /Tests/DAL/Users/Repository.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Artisan.Orm; 3 | using Microsoft.Data.SqlClient; 4 | using Tests.DAL.Users.Models; 5 | 6 | namespace Tests.DAL.Users; 7 | 8 | 9 | public class Repository: RepositoryBase 10 | { 11 | public Repository(string connectionString) : base(connectionString) { } 12 | 13 | 14 | #region [ GetUserById ] 15 | 16 | 17 | public User GetUserById(int id) 18 | { 19 | return GetByCommand(cmd => 20 | { 21 | cmd.UseProcedure("dbo.GetUserById"); 22 | 23 | cmd.AddIntParam("@Id", id); 24 | 25 | return cmd.ReadTo(); 26 | }); 27 | } 28 | 29 | 30 | public async Task GetUserByIdAsync(int id) 31 | { 32 | return await GetByCommandAsync(cmd => 33 | { 34 | cmd.UseProcedure("dbo.GetUserById"); 35 | 36 | cmd.AddIntParam("@Id", id); 37 | 38 | return cmd.ReadToAsync(); 39 | }); 40 | } 41 | 42 | 43 | public User GetUserByIdWithSql(int id) 44 | { 45 | return GetByCommand(cmd => 46 | { 47 | cmd.UseSql("select * from vwUsers where Id = @Id"); 48 | cmd.AddIntParam("@Id", id); 49 | 50 | return cmd.ReadTo(); 51 | }); 52 | } 53 | 54 | 55 | #endregion 56 | 57 | 58 | #region [ GetUsers ] 59 | 60 | 61 | public IList GetUsers() 62 | { 63 | return GetByCommand(cmd => 64 | { 65 | cmd.UseProcedure("dbo.GetUsers"); 66 | 67 | return cmd.ReadToList(); 68 | }); 69 | } 70 | 71 | public async Task> GetUsersAsync() 72 | { 73 | return await GetByCommandAsync(cmd => 74 | { 75 | cmd.UseProcedure("dbo.GetUsers"); 76 | 77 | return cmd.ReadToListAsync(); 78 | }); 79 | } 80 | 81 | 82 | #endregion 83 | 84 | 85 | #region [ GetUsers ] 86 | 87 | 88 | public ObjectRows GetUserRows() 89 | { 90 | return GetByCommand(cmd => 91 | { 92 | cmd.UseProcedure("dbo.GetUsers"); 93 | 94 | return cmd.ReadToObjectRows(); 95 | }); 96 | } 97 | 98 | public async Task GetUserRowsAsync() 99 | { 100 | return await GetByCommandAsync(cmd => 101 | { 102 | cmd.UseProcedure("dbo.GetUsers"); 103 | 104 | return cmd.ReadToObjectRowsAsync(); 105 | }); 106 | } 107 | 108 | 109 | public IList GetUsersWithRoles() 110 | { 111 | return GetByCommand(cmd => 112 | { 113 | cmd.UseSql( "select * from dbo.vwUsers; " + 114 | "select UserId, RoleId from dbo.UserRoles;" + 115 | "select Id, Code, Name from dbo.Roles"); 116 | 117 | return cmd.GetByReader(reader => 118 | { 119 | var users = reader.ReadToList(); 120 | var userRoles = reader.ReadToList(r => new {UserId = r.GetInt32(0), RoleId = r.GetByte(1)}); 121 | var roles = reader.ReadAsList(); 122 | 123 | reader.Close(); 124 | 125 | foreach (var user in users) 126 | { 127 | user.Roles = new List(); 128 | 129 | foreach (var role in roles) 130 | if (userRoles.Any(ur => ur.UserId == user.Id && ur.RoleId == role.Id)) 131 | user.Roles.Add(role); 132 | } 133 | 134 | return users; 135 | }); 136 | }); 137 | } 138 | 139 | 140 | #endregion 141 | 142 | 143 | #region [ Save ONE User ] 144 | 145 | 146 | public User SaveUser(User user) 147 | { 148 | return GetByCommand(cmd => 149 | { 150 | cmd.UseProcedure("dbo.SaveUser"); 151 | 152 | cmd.AddTableRowParam("@User", user); 153 | 154 | cmd.AddTableParam("@RoleIds", user.RoleIds); 155 | 156 | return cmd.GetByReader(ReadSavedUser); 157 | }); 158 | } 159 | 160 | public async Task SaveUserAsync(User user) 161 | { 162 | return await GetByCommandAsync(cmd => 163 | { 164 | cmd.UseProcedure("dbo.SaveUser"); 165 | 166 | cmd.AddTableRowParam("@User", user); 167 | 168 | cmd.AddTableParam("@RoleIds", user.RoleIds); 169 | 170 | return cmd.GetByReaderAsync(ReadSavedUser); 171 | }); 172 | } 173 | 174 | private static User ReadSavedUser(SqlDataReader reader) 175 | { 176 | CheckForDataReplyException(reader); 177 | 178 | return reader.ReadTo(); 179 | } 180 | 181 | 182 | #endregion 183 | 184 | 185 | #region [ Save MANY Users ] 186 | 187 | 188 | public IList SaveUsers(IList users) 189 | { 190 | return GetByCommand(cmd => 191 | { 192 | cmd.UseProcedure("dbo.SaveUsers"); 193 | 194 | cmd.AddTableParam("@Users", users); 195 | 196 | return cmd.GetByReader(ReadSavedUsers); 197 | }); 198 | } 199 | 200 | public async Task> SaveUsersAsync(IList users) 201 | { 202 | return await GetByCommandAsync(cmd => 203 | { 204 | cmd.UseProcedure("dbo.SaveUsers"); 205 | 206 | cmd.AddTableParam("@Users", users); 207 | 208 | return cmd.GetByReaderAsync(ReadSavedUsers); 209 | }); 210 | } 211 | 212 | 213 | private static IList ReadSavedUsers(SqlDataReader reader) 214 | { 215 | CheckForDataReplyException(reader); 216 | 217 | return reader.ReadToList(); 218 | } 219 | 220 | 221 | #endregion 222 | 223 | 224 | #region [ Delete User ] 225 | 226 | 227 | public bool DeleteUser(int userId) 228 | { 229 | //var returnValue = ExecuteCommand(cmd => 230 | //{ 231 | // cmd.UseProcedure("dbo.DeleteUser"); 232 | // cmd.AddIntParam("@UserId", userId); 233 | //}); 234 | 235 | var returnValue = Execute("dbo.DeleteUser", cmd => 236 | { 237 | cmd.AddIntParam("@UserId", userId); 238 | }); 239 | 240 | 241 | if (returnValue == 1) 242 | throw new DataReplyException(DataReplyStatus.Fail, "UNDELETABLE", "The Heros can not be deleted", userId); 243 | 244 | if (returnValue == 2) 245 | throw new DataReplyException(DataReplyStatus.Missing, "USER_IS_MISSING", userId); 246 | 247 | return true; 248 | } 249 | 250 | 251 | public async Task DeleteUserAsync(int userId) 252 | { 253 | //var returnValue = await ExecuteCommandAsync(cmd => 254 | //{ 255 | // cmd.UseProcedure("dbo.DeleteUser"); 256 | // cmd.AddIntParam("@UserId", userId); 257 | //}); 258 | 259 | var returnValue = await ExecuteAsync("dbo.DeleteUser", cmd => 260 | { 261 | cmd.AddIntParam("@UserId", userId); 262 | }); 263 | 264 | if (returnValue == 1) 265 | throw new DataReplyException(DataReplyStatus.Fail, "UNDELETABLE", "Heros can not be deleted", userId); 266 | 267 | if (returnValue == 2) 268 | throw new DataReplyException(DataReplyStatus.Missing, "USER_NOT_FOUND", userId); 269 | 270 | return true; 271 | } 272 | 273 | public void DeleteTwoUsers(int userId1, int userId2) 274 | { 275 | BeginTransaction(tran => 276 | { 277 | ExecuteCommand(cmd => 278 | { 279 | cmd.UseProcedure("dbo.DeleteUser"); 280 | cmd.AddIntParam("@UserId", userId1); 281 | }); 282 | 283 | ExecuteCommand(cmd => 284 | { 285 | cmd.UseProcedure("dbo.DeleteUser"); 286 | cmd.AddIntParam("@UserId", userId2); 287 | }); 288 | 289 | tran.Commit(); 290 | }); 291 | 292 | } 293 | 294 | 295 | public void CheckRuleForUser(int userId) 296 | { 297 | BeginTransaction(IsolationLevel.RepeatableRead, tran => 298 | { 299 | var user = GetByCommand(cmd => 300 | { 301 | cmd.UseProcedure("dbo.GetUserById"); 302 | cmd.AddIntParam("@Id", userId); 303 | 304 | return cmd.ReadTo(); 305 | }); 306 | 307 | if (Array.IndexOf(user.RoleIds, 2) > -1 && Array.IndexOf(user.RoleIds, 3) == -1) 308 | { 309 | var newRoleIds = user.RoleIds.ToList(); 310 | newRoleIds.Add(3); 311 | user.RoleIds = newRoleIds.ToArray(); 312 | } 313 | 314 | ExecuteCommand(cmd => 315 | { 316 | cmd.UseProcedure("dbo.SaveUser"); 317 | 318 | cmd.AddTableRowParam("@User", user); 319 | cmd.AddTableParam("@RoleIds", user.RoleIds); 320 | }); 321 | 322 | 323 | tran.Commit(); 324 | }); 325 | } 326 | 327 | 328 | 329 | // if to call this method like 330 | // await _repository.DeleteUserAsyncException(1); 331 | // it does not throw exception 332 | // 333 | //public async Task DeleteUserAsyncException(Int32 userId) 334 | //{ 335 | // await RunCommandAsync(async cmd => 336 | // { 337 | // cmd.UseProcedure("dbo.DeleteUser"); 338 | // cmd.AddIntParam("@UserId", userId); 339 | 340 | 341 | // var returnValueParam = cmd.ReturnValueParam(); 342 | 343 | // try 344 | // { 345 | // cmd.Connection.Open(); 346 | 347 | // await cmd.ExecuteNonQueryAsync(); 348 | // } 349 | // finally 350 | // { 351 | // cmd.Connection.Close(); 352 | // } 353 | 354 | // int returnValue = (int)returnValueParam.Value; 355 | 356 | // if (returnValue == 1) 357 | // throw new DataValidationException("UNDELETABLE", "Heros can not be deleted"); 358 | 359 | // }); 360 | //} 361 | 362 | 363 | #endregion 364 | 365 | } 366 | -------------------------------------------------------------------------------- /Tests/DataServices/DataServiceBase.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | 3 | namespace Tests.DataServices; 4 | 5 | public class DataServiceBase: IDisposable 6 | { 7 | internal IDisposable Repository; 8 | 9 | 10 | public static DataReply Get(Func func) 11 | { 12 | try 13 | { 14 | T data = func(); 15 | return new DataReply(data); 16 | } 17 | catch (DataReplyException ex) 18 | { 19 | return new DataReply(ex.Status, ex.Messages); 20 | } 21 | catch (Exception ex) 22 | { 23 | return new DataReply(DataReplyStatus.Error, GetErrorDataReplyMessages(ex)); 24 | } 25 | } 26 | 27 | public static async Task> GetAsync(Func> funcAsync) 28 | { 29 | try 30 | { 31 | T data = await funcAsync(); 32 | return new DataReply(data); 33 | } 34 | catch(DataReplyException ex) 35 | { 36 | return new DataReply(ex.Status, ex.Messages); 37 | } 38 | catch(Exception ex) 39 | { 40 | return new DataReply(DataReplyStatus.Error, GetErrorDataReplyMessages(ex)); 41 | } 42 | } 43 | 44 | public static DataReply Execute(Func func) 45 | { 46 | try 47 | { 48 | return func() ? new DataReply() : new DataReply(DataReplyStatus.Fail); 49 | } 50 | catch(DataReplyException ex) 51 | { 52 | return new DataReply(ex.Status, ex.Messages); 53 | } 54 | catch(Exception ex) 55 | { 56 | return new DataReply(DataReplyStatus.Error, GetErrorDataReplyMessages(ex)); 57 | } 58 | } 59 | 60 | 61 | public static async Task ExecuteAsync(Func> funcAsync) 62 | { 63 | try 64 | { 65 | return await funcAsync() ? new DataReply() : new DataReply(DataReplyStatus.Fail); 66 | } 67 | catch(DataReplyException ex) 68 | { 69 | return new DataReply(ex.Status, ex.Messages); 70 | } 71 | catch(Exception ex) 72 | { 73 | return new DataReply(DataReplyStatus.Error, GetErrorDataReplyMessages(ex)); 74 | } 75 | } 76 | 77 | 78 | private static DataReplyMessage [] GetErrorDataReplyMessages (Exception ex) 79 | { 80 | return new [] 81 | { 82 | new DataReplyMessage { Code = "ErrorMessage" , Text = ex.Message }, 83 | new DataReplyMessage { Code = "StackTrace" , Text = ex.StackTrace[..500] } 84 | }; 85 | } 86 | 87 | public void Dispose() 88 | { 89 | Repository?.Dispose(); 90 | Repository = null; 91 | 92 | GC.SuppressFinalize(this); 93 | } 94 | } 95 | 96 | -------------------------------------------------------------------------------- /Tests/DataServices/UserDataService.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | using Tests.DAL.Users; 3 | using Tests.DAL.Users.Models; 4 | 5 | namespace Tests.DataServices 6 | { 7 | public class UserDataService: DataServiceBase 8 | { 9 | private readonly Repository _repository; 10 | 11 | public UserDataService() 12 | { 13 | var appSettings = new AppSettings(); 14 | base.Repository = _repository = new Repository(appSettings.ConnectionStrings.DatabaseConnection); 15 | } 16 | 17 | 18 | // sync 19 | 20 | public DataReply GetById(int id) 21 | { 22 | return Get(() => _repository.GetUserById(id)); 23 | } 24 | 25 | public DataReply Save(User user) 26 | { 27 | return Get(() => _repository.SaveUser(user)); 28 | } 29 | 30 | 31 | public DataReply Delete(int userId) 32 | { 33 | return Execute(() => _repository.DeleteUser(userId)); 34 | } 35 | 36 | // async 37 | 38 | public async Task> GetByIdAsync(int id) 39 | { 40 | return await GetAsync(() => _repository.GetUserByIdAsync(id)); 41 | } 42 | 43 | public async Task> SaveAsync(User user) 44 | { 45 | return await GetAsync(() => _repository.SaveUserAsync(user)); 46 | } 47 | 48 | 49 | public async Task DeleteAsync(int userId) 50 | { 51 | return await ExecuteAsync(() => _repository.DeleteUserAsync(userId)); 52 | } 53 | 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | disable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Always 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Tests/Tests/ReadDictionaryTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Text.Json; 3 | using Artisan.Orm; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using Tests.DAL.Users.Models; 6 | 7 | namespace Tests.Tests 8 | { 9 | [TestClass] 10 | public class ReadDictionaryTest 11 | { 12 | private RepositoryBase _repositoryBase; 13 | 14 | [TestInitialize] 15 | public void TestInitialize() 16 | { 17 | var appSettings = new AppSettings(); 18 | 19 | _repositoryBase = new RepositoryBase(appSettings.ConnectionStrings.DatabaseConnection); 20 | } 21 | 22 | [TestMethod] 23 | public void GetDictionaryOfValues() 24 | { 25 | var sw = new Stopwatch(); 26 | sw.Start(); 27 | 28 | var dictionary = _repositoryBase.GetByCommand(cmd => 29 | { 30 | cmd.UseSql("select Id, Name from dbo.Roles"); 31 | 32 | return cmd.ReadToDictionary(); 33 | 34 | /* or 35 | 36 | return cmd.GetByReader(reader => 37 | { 38 | return reader.ReadToDictionary()); 39 | } 40 | 41 | */ 42 | 43 | }); 44 | 45 | sw.Stop(); 46 | 47 | Assert.IsNotNull(dictionary); 48 | 49 | Console.WriteLine($"Role name dictionary has been read for {sw.Elapsed.TotalMilliseconds:0.####} ms: "); 50 | Console.WriteLine(); 51 | Console.Write(JsonSerializer.Serialize(dictionary)); 52 | } 53 | 54 | [TestMethod] 55 | public void GetDictionaryOfObjects() 56 | { 57 | var sw = new Stopwatch(); 58 | sw.Start(); 59 | 60 | var dictionary = _repositoryBase.ReadToDictionary("select * from dbo.Roles"); 61 | 62 | //var dictionary = _repositoryBase.GetByCommand(cmd => 63 | //{ 64 | // cmd.UseSql("select * from dbo.Roles"); 65 | // return cmd.ReadToDictionary(); 66 | //}); 67 | 68 | sw.Stop(); 69 | 70 | Assert.IsNotNull(dictionary); 71 | Assert.IsTrue(dictionary.Count > 1); 72 | 73 | Console.WriteLine($"Role object dictionary has been read for {sw.Elapsed.TotalMilliseconds:0.####} ms: "); 74 | Console.WriteLine(); 75 | Console.Write(JsonSerializer.Serialize(dictionary)); 76 | 77 | } 78 | 79 | [TestMethod] 80 | public async Task GetDictionaryOfObjectsAsync() 81 | { 82 | var sw = new Stopwatch(); 83 | sw.Start(); 84 | 85 | var records = await _repositoryBase.ReadToDictionaryAsync("select * from dbo.Roles"); 86 | 87 | sw.Stop(); 88 | 89 | Assert.IsNotNull(records); 90 | Assert.IsTrue(records.Count > 1); 91 | 92 | Console.WriteLine($"GetDictionaryOfObjectsAsync reads {records.Count} roles for {sw.Elapsed.TotalMilliseconds:0.##} ms"); 93 | Console.Write(JsonSerializer.Serialize(records)); 94 | } 95 | 96 | [TestMethod] 97 | public void GetDictionaryWithHandmapping() 98 | { 99 | var sw = new Stopwatch(); 100 | sw.Start(); 101 | 102 | var dictionary = _repositoryBase.GetByCommand(cmd => 103 | { 104 | cmd.UseSql("select * from dbo.Roles"); 105 | 106 | return cmd.ReadToDictionary(dr => new Role 107 | { 108 | Id = dr.GetByte(0) , 109 | Code = dr.GetString(1) , 110 | Name = dr.GetString(2) 111 | }); 112 | 113 | }); 114 | 115 | sw.Stop(); 116 | 117 | Assert.IsNotNull(dictionary); 118 | Assert.IsTrue(dictionary.Count > 1); 119 | 120 | Console.WriteLine($"Role object dictionary has been read with Handmapping for {sw.Elapsed.TotalMilliseconds:0.####} ms: "); 121 | Console.WriteLine(); 122 | Console.Write(JsonSerializer.Serialize(dictionary)); 123 | 124 | } 125 | 126 | [TestMethod] 127 | public void GetDictionaryWithAutomapping() 128 | { 129 | _ = _repositoryBase.ReadAsDictionary("select * from dbo.Roles"); 130 | 131 | var sw = new Stopwatch(); 132 | sw.Start(); 133 | 134 | IDictionary dictionary = _repositoryBase.ReadAsDictionary("select * from dbo.Roles"); 135 | 136 | //var dictionary = _repositoryBase.GetByCommand(cmd => 137 | //{ 138 | // cmd.UseSql("select * from dbo.Roles"); 139 | // return cmd.ReadToDictionary(); 140 | //}); 141 | 142 | sw.Stop(); 143 | 144 | Assert.IsNotNull(dictionary); 145 | Assert.IsTrue(dictionary.Count > 1); 146 | 147 | Console.WriteLine($"Role object dictionary has been read with Automapping for {sw.Elapsed.TotalMilliseconds:0.####} ms: "); 148 | Console.WriteLine(); 149 | Console.Write(JsonSerializer.Serialize(dictionary)); 150 | 151 | } 152 | 153 | [TestCleanup] 154 | public void Dispose() 155 | { 156 | _repositoryBase.Dispose(); 157 | } 158 | 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Tests/Tests/UserDataServiceTest.cs: -------------------------------------------------------------------------------- 1 | using Artisan.Orm; 2 | using Tests.DAL.Users; 3 | using Tests.DataServices; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Tests.Tests 7 | { 8 | [TestClass] 9 | public class UserDataServiceTest 10 | { 11 | private UserDataService _service; 12 | 13 | [TestInitialize] 14 | public void TestInitialize() 15 | { 16 | _service = new UserDataService(); 17 | 18 | var appSettings = new AppSettings(); 19 | 20 | using (var repository = new Repository(appSettings.ConnectionStrings.DatabaseConnection)) 21 | { 22 | repository.ExecuteCommand(cmd => { 23 | cmd.UseSql("delete from dbo.Users where Id > 14;"); 24 | }); 25 | }; 26 | 27 | } 28 | 29 | [TestMethod] 30 | public void GetSaveDeleteUser() 31 | { 32 | var dataReplyT = _service.GetById(1); 33 | 34 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 35 | 36 | var user = dataReplyT.Data; 37 | 38 | user.Id = 0; 39 | user.Login = $"{user.Login}a"; 40 | user.Name = $"{user.Name}a"; 41 | user.Email = $"a{user.Email}"; 42 | 43 | dataReplyT = _service.Save(user); 44 | 45 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 46 | 47 | user = dataReplyT.Data; 48 | 49 | Assert.IsNotNull(user); 50 | Assert.IsTrue(user.Id > 0); 51 | 52 | var dataReply = _service.Delete(user.Id); 53 | 54 | Assert.AreEqual(DataReplyStatus.Ok, dataReply.Status); 55 | } 56 | 57 | [TestMethod] 58 | public async Task GetSaveDeleteUserAsync() 59 | { 60 | var dataReplyT = await _service.GetByIdAsync(1); 61 | 62 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 63 | 64 | var user = dataReplyT.Data; 65 | 66 | user.Id = 0; 67 | user.Login = $"{user.Login}a"; 68 | user.Name = $"{user.Name}a"; 69 | user.Email = $"a{user.Email}"; 70 | 71 | dataReplyT = await _service.SaveAsync(user); 72 | 73 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 74 | 75 | user = dataReplyT.Data; 76 | 77 | Assert.IsNotNull(user); 78 | Assert.IsTrue(user.Id > 0); 79 | 80 | var dataReply = await _service.DeleteAsync(user.Id); 81 | 82 | Assert.AreEqual(DataReplyStatus.Ok, dataReply.Status); 83 | } 84 | 85 | 86 | [TestMethod] 87 | public async Task ConcurrencyDataReplyStatusAsync() 88 | { 89 | var dataReplyT = await _service.GetByIdAsync(1); 90 | 91 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 92 | 93 | var user = dataReplyT.Data; 94 | 95 | user.Id = 0; 96 | user.Login = $"{user.Login}b"; 97 | user.Name = $"{user.Name}b"; 98 | user.Email = $"b{user.Email}"; 99 | 100 | dataReplyT = await _service.SaveAsync(user); 101 | 102 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 103 | 104 | user = dataReplyT.Data; 105 | 106 | var dataReplyTConcurrent = await _service.GetByIdAsync(user.Id); 107 | 108 | var userConcurrent = dataReplyTConcurrent.Data; 109 | 110 | user.Name = $"{user.Name}c"; 111 | 112 | dataReplyT = await _service.SaveAsync(user); 113 | 114 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 115 | Assert.AreEqual(user.Name, dataReplyT.Data.Name); 116 | 117 | 118 | userConcurrent.Name = $"{user.Name}d"; 119 | 120 | dataReplyTConcurrent = await _service.SaveAsync(userConcurrent); 121 | 122 | Assert.AreEqual(DataReplyStatus.Concurrency, dataReplyTConcurrent.Status); 123 | 124 | 125 | var dataReply = await _service.DeleteAsync(user.Id); 126 | 127 | Assert.AreEqual(DataReplyStatus.Ok, dataReply.Status); 128 | } 129 | 130 | 131 | [TestMethod] 132 | public void DeleteUndeletableUser() 133 | { 134 | var userId = 1; 135 | 136 | var dataReply = _service.Delete(userId); 137 | 138 | Assert.AreEqual(DataReplyStatus.Fail, dataReply.Status); 139 | 140 | Assert.AreEqual("UNDELETABLE", dataReply.Messages[0].Code); 141 | 142 | Assert.AreEqual(userId, dataReply.Messages[0].Id); 143 | } 144 | 145 | 146 | [TestMethod] 147 | public void DeleteMissingUser() 148 | { 149 | var userId = 10000000; 150 | 151 | var dataReply = _service.Delete(userId); 152 | 153 | Assert.AreEqual(DataReplyStatus.Missing, dataReply.Status); 154 | 155 | Assert.AreEqual("USER_IS_MISSING", dataReply.Messages[0].Code); 156 | 157 | Assert.AreEqual(userId, dataReply.Messages[0].Id); 158 | } 159 | 160 | [TestMethod] 161 | public void SaveUserValidation() 162 | { 163 | var dataReplyT = _service.GetById(1); 164 | 165 | Assert.AreEqual(DataReplyStatus.Ok, dataReplyT.Status); 166 | 167 | var user = dataReplyT.Data; 168 | 169 | user.Id = 0; 170 | 171 | dataReplyT = _service.Save(user); 172 | 173 | Assert.AreEqual(DataReplyStatus.Validation, dataReplyT.Status); 174 | 175 | Assert.AreEqual("NON_UNIQUE_LOGIN", dataReplyT.Messages[0].Code); 176 | 177 | Assert.AreEqual("NON_UNIQUE_NAME", dataReplyT.Messages[1].Code); 178 | 179 | Assert.AreEqual("NON_UNIQUE_EMAIL", dataReplyT.Messages[2].Code); 180 | 181 | Assert.IsNull(dataReplyT.Data); 182 | } 183 | 184 | 185 | 186 | [TestCleanup] 187 | public void Dispose() 188 | { 189 | _service.Dispose(); 190 | } 191 | 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Tests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DatabaseConnection": "Data Source=.\\SQLEXPRESS;Initial Catalog=Artisan;Integrated Security=True;Trust Server Certificate=True" 4 | } 5 | } --------------------------------------------------------------------------------