GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
DatabaseHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Generic;
19using System.Data;
20using System.Data.SqlClient;
21using System.Drawing;
22using System.Linq;
23using System.Text;
24using System.Threading.Tasks;
25using static GenerallyPositive.Enums;
26
27namespace GenerallyPositive
28{
29 internal class DatabaseHelper
30 {
31 public static SqlConnection OpenConnection(DatabaseSettings databaseSettings)
32 {
33 if (null == databaseSettings.Connection)
34 {
35 try
36 {
37 if (null != databaseSettings.ConnectionString)
38 databaseSettings.Connection = new SqlConnection(databaseSettings.ConnectionString);
39 else if (Enums.DatabaseType.SQLServer == databaseSettings.DatabaseType)
40 {
41 StringBuilder sb = new StringBuilder();
42 sb.Append($"Data Source={databaseSettings.ServerName};Initial Catalog={databaseSettings.DatabaseName};");
43 if (null == databaseSettings.UserName)
44 sb.Append("Integrated Security=true;");
45 else
46 {
47 sb.Append($"User id={databaseSettings.UserName};password={databaseSettings.Password};");
48 }
49 databaseSettings.Connection = new SqlConnection(sb.ToString());
50
51 }
52 databaseSettings.Connection.Open();
53 }
54 catch (Exception ex)
55 {
56 RecordFailure(databaseSettings, ex, $"Unable to open a connection to [{databaseSettings.DatabaseName}] on [{databaseSettings.ServerName}]", null, null, -1);
57 }
58 }
59 return databaseSettings.Connection;
60 }
61 public static void Close(DatabaseSettings databaseSettings)
62 {
63 // a connection carrying a transaction stays open. closing it here would roll back between two
64 // statements the workflow meant to keep together
65 if (null == databaseSettings.SqlTransaction)
66 {
67 databaseSettings.Connection.Close();
68 databaseSettings.Connection = null;
69 }
70 }
71
78 public static void CloseEverything(DatabaseSettings databaseSettings)
79 {
80 if (null != databaseSettings.SqlTransaction)
81 {
82 databaseSettings.SqlTransaction.Rollback();
83 databaseSettings.SqlTransaction = null;
84 }
85
86 if (null != databaseSettings.Connection)
87 {
88 if (ConnectionState.Closed != databaseSettings.Connection.State)
89 databaseSettings.Connection.Close();
90
91 databaseSettings.Connection.Dispose();
92 databaseSettings.Connection = null;
93 }
94 }
102 private static void PrepareCommand(DatabaseSettings databaseSettings, SqlCommand command)
103 {
104 if (null != command)
105 {
106 if (0 <= databaseSettings.CommandTimeoutSeconds)
107 command.CommandTimeout = databaseSettings.CommandTimeoutSeconds;
108
109 if (null != databaseSettings.SqlTransaction)
110 command.Transaction = databaseSettings.SqlTransaction;
111 }
112 }
113
122 private static SqlCommand BuildReadCommand(DatabaseSettings databaseSettings, SqlConnection sqlConnection)
123 {
124 SqlCommand cmd = null;
125
126 if (null != databaseSettings.SqlCommand)
127 {
128 cmd = new SqlCommand(databaseSettings.SqlCommand, sqlConnection)
129 {
130 CommandType = databaseSettings.SqlCommandType
131 };
132 }
133 else if (null != databaseSettings.ReadSQL)
134 {
135 cmd = new SqlCommand(databaseSettings.ReadSQL, sqlConnection)
136 {
137 CommandType = databaseSettings.SqlCommandType
138 };
139 }
140 else if (null != databaseSettings.TableName)
141 {
142 string sqlCommand = "Select " + ((-1 != databaseSettings.RowCount) ? $"TOP({databaseSettings.RowCount}) " : "") + $"* from {databaseSettings.TableName}";
143
144 cmd = new SqlCommand(sqlCommand, sqlConnection)
145 {
146 CommandType = CommandType.Text
147 };
148 }
149 else if (null != databaseSettings.StoredProcedure)
150 {
151 int parmIdx = 0;
152 cmd = sqlConnection.CreateCommand();
153 cmd.CommandType = CommandType.StoredProcedure;
154 cmd.CommandText = databaseSettings.StoredProcedure;
155 if (null != databaseSettings.ParametersList)
156 {
157 foreach (string parameter in databaseSettings.ParameterNamesList)
158 cmd.Parameters.Add(new SqlParameter(parameter, databaseSettings.ParametersList[parmIdx++]));
159 }
160 }
161
162 PrepareCommand(databaseSettings, cmd);
163
164 return cmd;
165 }
166
173 public static void GetScalar(DatabaseSettings databaseSettings, out string value)
174 {
175 string retVal = null;
176 SqlConnection sqlConnection = OpenConnection(databaseSettings);
177 SqlCommand cmd = BuildReadCommand(databaseSettings, sqlConnection);
178
179 try
180 {
181 object answer = cmd?.ExecuteScalar();
182
183 retVal = DBNull.Value == answer ? null : answer?.ToString();
184
185 Close(databaseSettings);
186 }
187 catch (Exception ex)
188 {
189 RecordFailure(databaseSettings, ex, $"Failed reading with [{cmd?.CommandText}]", cmd?.CommandText, databaseSettings.ParametersGrid, -1);
190 }
191
192 value = retVal;
193 }
194
201 public static int GetDataSet(DatabaseSettings databaseSettings, out DataSet dataSet)
202 {
203 DataSet returnDataSet = new DataSet();
204 SqlConnection sqlConnection = OpenConnection(databaseSettings);
205 SqlCommand cmd = BuildReadCommand(databaseSettings, sqlConnection);
206
207 try
208 {
209 using var adapter = new SqlDataAdapter(cmd);
210 adapter.Fill(returnDataSet);
211 Close(databaseSettings);
212 }
213 catch (Exception ex)
214 {
215 RecordFailure(databaseSettings, ex, $"Failed reading with [{cmd?.CommandText}]", cmd?.CommandText, databaseSettings.ParametersGrid, -1);
216 }
217
218 dataSet = returnDataSet;
219 return dataSet.Tables.Count;
220 }
225 public static int GetGrid(DatabaseSettings databaseSettings, out IGPALGrid<string> dataGrid)
226 {
227 IGPALGrid<string> returnDataGrid = GPAL.Grid.ToGPALObject();
228 GetDataSet(databaseSettings, out DataSet dataSet);
229
230 var rowCountsPerTable = new List<int>();
231 int rowsAdded = 0;
232 foreach (DataTable table in dataSet.Tables)
233 {
234 if (databaseSettings.ColumnList.Count == 0)
235 {
236 foreach (DataColumn column in table.Columns)
237 {
238 SqlDbType sqlDbType = GetSqlDbTypeFromClrType(column.DataType);
239 databaseSettings.ColumnList.Add(new DatabaseSettings.DatabaseColumn(column.ColumnName, sqlDbType));
240 }
241 }
242
243 int rowsAddedForTable = 0;
244 foreach (DataRow row in table.Rows)
245 {
246 if (0 < databaseSettings.RowCount && rowsAdded >= databaseSettings.RowCount)
247 break;
248 returnDataGrid.AddRow(row.ItemArray.Select(v => v.ToString()).ToList());
249 rowsAdded++;
250 rowsAddedForTable++;
251 }
252 rowCountsPerTable.Add(rowsAddedForTable);
253 }
254 databaseSettings.LastGridRowCountsPerTable = rowCountsPerTable;
255
256 dataGrid = returnDataGrid;
257 return dataGrid.Count();
258 }
263 public static int GetDataTable(DatabaseSettings databaseSettings, out DataTable dataTable)
264 {
265 GetDataSet(databaseSettings, out DataSet dataSet);
266
267 if (dataSet.Tables.Count > 1)
268 {
269 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
270 $"Query returned [{dataSet.Tables.Count}] result sets; SaveTo(out DataTable) only keeps the first. Use SaveTo(out DataSet) to get all of them.",
271 databaseSettings.Database, GPALObjectType.Database);
272 }
273
274 dataTable = dataSet.Tables.Count > 0 ? dataSet.Tables[0] : new DataTable();
275 return dataTable.Rows.Count;
276 }
277 public static int Insert(DatabaseSettings databaseSettings, IGPALGrid<string> tokens)
278 {
279 StringBuilder insertCommand = new StringBuilder();
280 StringBuilder insertParams = new StringBuilder();
281
282 insertCommand.Append($"INSERT INTO {databaseSettings.TableName} (");
283
284 foreach (DatabaseSettings.DatabaseColumn databaseColumn in databaseSettings.ColumnList)
285 {
286 insertCommand.Append($"{databaseColumn.ColumName}, ");
287 insertParams.Append($"@{databaseColumn.ColumName}, ");
288 }
289 insertCommand.Remove(insertCommand.Length - 1, 1); // remove trailing comma
290 insertCommand.Append(") Values (");
291 insertParams.Remove(insertParams.Length - 1, 1); // remove trailing comma
292 insertParams.Append(")");
293 insertCommand.Append(insertParams);
294
295 return ExecuteSQL(databaseSettings, insertCommand.ToString(), tokens);
296 }
305 internal static int ExecuteSQL(DatabaseSettings databaseSettings, string sqlToRun, IGPALGrid<string> parameterTokens = null)
306 {
307 return ExecuteSQL(databaseSettings, sqlToRun, parameterTokens, out _);
308 }
311 internal static int ExecuteSQL(DatabaseSettings databaseSettings, string sqlToRun, IGPALGrid<string> parameterTokens, out List<int> rowCountsPerStatement)
312 {
313 using var sqlCommand = new SqlCommand(sqlToRun, databaseSettings.Connection ?? OpenConnection(databaseSettings))
314 {
315 CommandType = databaseSettings.SqlCommandType
316 };
317
318 PrepareCommand(databaseSettings, sqlCommand);
319
320 int rowCount = 0;
321 int rowBeingWritten = -1;
322 bool ranPerRow = false;
323 var statementCounts = new List<int>();
324
325 // cleared here so it always describes this call rather than an older one
326 databaseSettings.LastError = null;
327 sqlCommand.StatementCompleted += (s, e) => statementCounts.Add(e.RecordCount);
328
329 try
330 {
331 if (parameterTokens != null && parameterTokens.Rows > 0)
332 {
333 var structuredColumn = databaseSettings.ColumnList.FirstOrDefault(c => c.ColumnType == SqlDbType.Structured);
334 if (structuredColumn != null)
335 {
336 if (string.IsNullOrEmpty(structuredColumn.TvTableTypeName))
337 throw new InvalidOperationException($"UDTT Type Name required for Table Value column @[{structuredColumn.ColumName}]. Use .WithUDDTName()");
338
339 var dataTable = new DataTable();
340 int columnCount = parameterTokens.Columns;
341
342 for (int i = 0; i < columnCount; i++)
343 {
344 dataTable.Columns.Add($"Column{i}", typeof(string));
345 }
346
347 foreach (var row in parameterTokens)
348 {
349 if (row.Count != columnCount)
350 {
351 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
352 $"Row [{row}] has [{row.Count}] values, but expected [{columnCount}]. Skipping row.",
353 parameterTokens, GPALObjectType.Other);
354 continue;
355 }
356 dataTable.Rows.Add(row.ToArray());
357 }
358
359 var param = sqlCommand.Parameters.AddWithValue($"@{structuredColumn.ColumName}", dataTable);
360 param.SqlDbType = SqlDbType.Structured;
361 param.TypeName = structuredColumn.TvTableTypeName;
362 }
363 else
364 {
365 // a grid is rows of parameters, so the command runs once against each of them. one row is
366 // the ordinary case and runs once, which is what a caller adding parameters one at a time
367 // gets. taking only the first row meant a grid of fifty rows wrote one and warned about
368 // the other forty nine
369 foreach (var parameters in parameterTokens)
370 {
371 rowBeingWritten++;
372 sqlCommand.Parameters.Clear();
373
374 foreach (var databaseColumn in databaseSettings.ColumnList)
375 {
376 int columnIndex = databaseSettings.ColumnList.IndexOf(databaseColumn);
377 string paramValue = parameters.Count > columnIndex ? parameters[columnIndex] : null;
378 var convertedValue = ReturnParameterOfType(paramValue, databaseColumn.ColumnType);
379 sqlCommand.Parameters.Add($"@{databaseColumn.ColumName}", databaseColumn.ColumnType, databaseColumn.ColumnLength).Value = convertedValue;
380 }
381
382 rowCount += sqlCommand.ExecuteNonQuery();
383 }
384
385 ranPerRow = true;
386 }
387 }
388
389 // the table valued path hands the whole grid over as one command, and a command with no
390 // parameters at all still has to run
391 if (false == ranPerRow)
392 rowCount = sqlCommand.ExecuteNonQuery();
393 }
394 catch (Exception ex)
395 {
396
397 RecordFailure(databaseSettings, ex, $"Failed running [{sqlToRun}]", sqlToRun, parameterTokens, rowBeingWritten);
398 }
399
400 rowCountsPerStatement = statementCounts;
401 return rowCount;
402 }
413 private static void RecordFailure(DatabaseSettings databaseSettings, Exception ex, string message, string sql, IGPALGrid<string> parameterTokens, int row)
414 {
415 databaseSettings.LastError = new GPALDatabase.DatabaseError
416 {
417 Number = ex is SqlException sqlException ? sqlException.Number : 0,
418 Message = ex.Message,
419 Sql = sql,
420 Parameters = parameterTokens,
421 Row = row,
422 RowValues = 0 <= row && row < (parameterTokens?.Rows ?? 0) ? new List<string>(parameterTokens[row]) : null,
423 Occurred = DateTime.Now,
424 Exception = ex
425 };
426
427 // the server rejecting something is an error and says so with a number and a message. anything else
428 // arriving here was not expected at all, so it stays an exception.
429 // published rather than handed straight to the exception handler: this reaches the console, the debug
430 // window and a logger as well, the handler still fires because an exception came with it, and the
431 // screenshot is taken by the publish rather than here
432 GPAL.PublishSimpleEvent(ex is SqlException ? GPALEventType.ERROR : GPALEventType.EXCEPTION,
433 message, databaseSettings.Database, GPALObjectType.Database, ex);
434
435 CallOnErrorHandler(databaseSettings);
436 }
437
438 private static void CallOnErrorHandler(DatabaseSettings databaseSettings)
439 {
440 if (null != databaseSettings.CallOnErrorHandler)
441 databaseSettings.CallOnErrorHandler(databaseSettings.Database);
442
443 if (null != databaseSettings.SqlTransaction)
444 {
445 databaseSettings.SqlTransaction.Rollback();
446 databaseSettings.SqlTransaction = null;
447 }
448
449 if (null != databaseSettings.Connection && ConnectionState.Open == databaseSettings.Connection.State)
450 {
451 databaseSettings.Connection.Close();
452 databaseSettings.Connection = null;
453 }
454 }
455 static SqlDbType GetSqlDbTypeFromName(string dataTypeName)
456 {
457 switch (dataTypeName.ToLower())
458 {
459 case "char":
460 return SqlDbType.Char;
461 case "nchar":
462 return SqlDbType.NChar;
463 case "ntext":
464 return SqlDbType.NText;
465 case "nvarchar":
466 return SqlDbType.NVarChar;
467 case "text":
468 return SqlDbType.Text;
469 case "varchar":
470 return SqlDbType.VarChar;
471 case "xml":
472 return SqlDbType.Xml;
473 case "date":
474 return SqlDbType.Date;
475 case "datetime":
476 return SqlDbType.DateTime;
477 case "datetime2":
478 return SqlDbType.DateTime2;
479 case "smalldatetime":
480 return SqlDbType.SmallDateTime;
481 case "time":
482 return SqlDbType.Time;
483 case "timestamp":
484 return SqlDbType.Timestamp;
485 case "decimal":
486 return SqlDbType.Decimal;
487 case "money":
488 return SqlDbType.Money;
489 case "smallmoney":
490 return SqlDbType.SmallMoney;
491 case "float":
492 return SqlDbType.Float;
493 case "real":
494 return SqlDbType.Real;
495 case "image":
496 return SqlDbType.Image;
497 case "bit":
498 return SqlDbType.Bit;
499 case "int":
500 return SqlDbType.Int;
501 case "smallint":
502 return SqlDbType.SmallInt;
503 // Add cases for other types as needed
504 default:
505 throw new ArgumentException("Unknown SqlDbType: " + dataTypeName);
506 }
507 }
512 static SqlDbType GetSqlDbTypeFromClrType(Type clrType)
513 {
514 switch (Type.GetTypeCode(clrType))
515 {
516 case TypeCode.String: return SqlDbType.NVarChar;
517 case TypeCode.Boolean: return SqlDbType.Bit;
518 case TypeCode.Byte: return SqlDbType.TinyInt;
519 case TypeCode.Int16: return SqlDbType.SmallInt;
520 case TypeCode.Int32: return SqlDbType.Int;
521 case TypeCode.Int64: return SqlDbType.BigInt;
522 case TypeCode.Single: return SqlDbType.Real;
523 case TypeCode.Double: return SqlDbType.Float;
524 case TypeCode.Decimal: return SqlDbType.Decimal;
525 case TypeCode.DateTime: return SqlDbType.DateTime;
526 default:
527 if (clrType == typeof(Guid)) return SqlDbType.UniqueIdentifier;
528 if (clrType == typeof(byte[])) return SqlDbType.VarBinary;
529 if (clrType == typeof(TimeSpan)) return SqlDbType.Time;
530 if (clrType == typeof(DateTimeOffset)) return SqlDbType.DateTimeOffset;
531 return SqlDbType.NVarChar;
532 }
533 }
534 private static object ReturnParameterOfType(string parmValue, SqlDbType sqlDbType)
535 {
536 if (string.IsNullOrEmpty(parmValue))
537 {
538 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
539 $"Parameter value is null or empty for SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
540 null, GPALObjectType.Other);
541 return DBNull.Value;
542 }
543
544 try
545 {
546 switch (sqlDbType)
547 {
548 case SqlDbType.Char:
549 case SqlDbType.NChar:
550 case SqlDbType.NText:
551 case SqlDbType.NVarChar:
552 case SqlDbType.Text:
553 case SqlDbType.VarChar:
554 case SqlDbType.Xml:
555 return parmValue;
556
557 case SqlDbType.Date:
558 case SqlDbType.DateTime:
559 case SqlDbType.DateTime2:
560 case SqlDbType.SmallDateTime:
561 case SqlDbType.Time:
562 return DateTime.TryParse(parmValue, out DateTime dateResult) ? (object)dateResult : DBNull.Value;
563
564 case SqlDbType.DateTimeOffset:
565 return DateTimeOffset.TryParse(parmValue, out DateTimeOffset offsetResult) ? (object)offsetResult : DBNull.Value;
566
567 case SqlDbType.Decimal:
568 case SqlDbType.Money:
569 case SqlDbType.SmallMoney:
570 return Decimal.TryParse(parmValue, out decimal decimalResult) ? (object)decimalResult : DBNull.Value;
571
572 case SqlDbType.Float:
573 case SqlDbType.Real:
574 return float.TryParse(parmValue, out float floatResult) ? (object)floatResult : DBNull.Value;
575
576 case SqlDbType.Bit:
577 return bool.TryParse(parmValue, out bool boolResult) ? (object)boolResult : (int.TryParse(parmValue, out int intBitResult) ? (object)(intBitResult != 0) : DBNull.Value);
578
579 case SqlDbType.Int:
580 case SqlDbType.SmallInt:
581 return int.TryParse(parmValue, out int intResult) ? (object)intResult : DBNull.Value;
582
583 case SqlDbType.BigInt:
584 return long.TryParse(parmValue, out long longResult) ? (object)longResult : DBNull.Value;
585
586 case SqlDbType.UniqueIdentifier:
587 return Guid.TryParse(parmValue, out Guid guidResult) ? (object)guidResult : DBNull.Value;
588
589 case SqlDbType.Image:
590 case SqlDbType.VarBinary:
591 case SqlDbType.Binary:
592 try
593 {
594 return Convert.FromBase64String(parmValue);
595 }
596 catch (FormatException ex)
597 {
598 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
599 $"Failed to decode Base64 string [{parmValue}] for SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
600 null, GPALObjectType.Other, ex);
601 return DBNull.Value;
602 }
603
604 case SqlDbType.Timestamp:
605 try
606 {
607 if (ulong.TryParse(parmValue, out ulong ulongResult))
608 {
609 return BitConverter.GetBytes(ulongResult).Reverse().ToArray();
610 }
611 throw new FormatException("Invalid numeric string for Timestamp");
612 }
613 catch (Exception ex)
614 {
615 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
616 $"Failed to parse [{parmValue}] as Timestamp. Returning DBNull.Value.",
617 null, GPALObjectType.Other, ex);
618 return DBNull.Value;
619 }
620
621 case SqlDbType.Variant:
622 if (int.TryParse(parmValue, out int variantIntResult))
623 return variantIntResult;
624 if (decimal.TryParse(parmValue, out decimal variantDecResult))
625 return variantDecResult;
626 if (DateTime.TryParse(parmValue, out DateTime variantDateResult))
627 return variantDateResult;
628 if (Guid.TryParse(parmValue, out Guid variantGuidResult))
629 return variantGuidResult;
630 return parmValue;
631
632 case SqlDbType.Structured:
633 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
634 $"SqlDbType.Structured is not supported for individual parameter values. Use a multi-row GPALGrid<string> in ExecuteSQL. Returning DBNull.Value.",
635 null, GPALObjectType.Other);
636 return DBNull.Value;
637
638 default:
639 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
640 $"Unsupported SqlDbType [{sqlDbType}] for parameter value [{parmValue}]. Returning DBNull.Value.",
641 null, GPALObjectType.Other);
642 return DBNull.Value;
643 }
644 }
645 catch (Exception ex)
646 {
647 GPAL.PublishSimpleEvent(GPALEventType.WARNING,
648 $"Failed to convert parameter value [{parmValue}] to SqlDbType [{sqlDbType}]. Returning DBNull.Value.",
649 null, GPALObjectType.Other, ex);
650 return DBNull.Value;
651 }
652 }
657 internal static void TokenizeDatabase(UnitOfWork currentUOW, IGPALDatabase inputDatabase)
658 {
659 IGPALGrid<string> tokens;
660 if (null != currentUOW)
661 currentUOW.InputDatabase = inputDatabase;
662 int rowCount = DatabaseHelper.GetGrid(((GPALDatabase)inputDatabase).DatabaseSettings, out tokens);
663 ((GPALDatabase)inputDatabase).Tokens = tokens;
664 }
672 public static IGPALGrid<string> DataTableToGrid(DataTable dataTable, out List<DatabaseSettings.DatabaseColumn> columnsList)
673 {
674 IGPALGrid<string> retVal = GPAL.Grid.ToGPALObject();
675
676 columnsList = new List<DatabaseSettings.DatabaseColumn>();
677
678 if (null != dataTable)
679 {
680 foreach (DataColumn column in dataTable.Columns)
681 columnsList.Add(new DatabaseSettings.DatabaseColumn(column.ColumnName, GetSqlDbTypeFromClrType(column.DataType)));
682
683 foreach (DataRow row in dataTable.Rows)
684 retVal.AddRow(row.ItemArray.Select(value => DBNull.Value == value ? null : value?.ToString()).ToList());
685 }
686
687 return retVal;
688 }
689 public static void TokenizeDatabase(GPALDatabase inputDatabase)
690 {
691 IGPALGrid<string> tokens;
692 int rowCount = DatabaseHelper.GetGrid(inputDatabase.DatabaseSettings, out tokens);
693 inputDatabase.Tokens = tokens;
694 }
695 }
696
701 public static class DataTableExtensions
702 {
703 public static void SetColumnsOrder(this DataTable table, params String[] columnNames)
704 {
705 int columnIndex = 0;
706 foreach (var columnName in columnNames)
707 {
708 if (table.Columns.Contains(columnName))
709 {
710 table.Columns[columnName].SetOrdinal(columnIndex);
711 columnIndex++;
712 }
713 }
714 }
715 }
716}
717