using System; using System.Collections.Generic; using System.Data.Linq; using System.Data.SqlClient; using System.Linq; namespace Extensions { /// Provides helper functions to deal with SQL 2008 Change Tracking and LINQ public static class ChangeTracking { public static long GetCurrentVersion(DataContext dc) { var ver = dc.ExecuteQuery("SELECT CHANGE_TRACKING_CURRENT_VERSION()").First(); if (!ver.HasValue) throw new InvalidOperationException("Change tracking is not enabled on this database."); return ver.Value; } public static long GetValidVersion(DataContext dc, string table) { var ver = dc.ExecuteQuery("SELECT CHANGE_TRACKING_MIN_VALID_VERSION(OBJECT_ID('" + table + "'))").First(); if (!ver.HasValue) throw new ArgumentException("Change tracking version not available for specified table."); return ver.Value; } public static long GetValidVersionForAll(DataContext dc) { var ver = dc.ExecuteQuery("SELECT MAX(min_valid_version) FROM sys.change_tracking_tables").First(); if (!ver.HasValue) throw new ArgumentException("Change tracking version not found."); return ver.Value; } public static System.Transactions.TransactionScope GetSnapshotScope() { var txOptions = new System.Transactions.TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Snapshot }; return new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Required, txOptions); } #region Updated/Inserted public static IEnumerable GetChangedItems(DataContext dc, IQueryable query, string tableName, string keyColumn, long lastVersion) { string join = " INNER JOIN " + getChangeTable(tableName, lastVersion) + " ON [t0]." + keyColumn + " = CT." + keyColumn + " AND " + sysOpNotDel; return getChangedItems(dc, query, tableName, lastVersion, join); } public static IEnumerable GetChangedItems(DataContext dc, IQueryable query, string tableName, string keyColumn1, string keyColumn2, long lastVersion) { string join = " INNER JOIN " + getChangeTable(tableName, lastVersion) + " ON [t0]." + keyColumn1 + " = CT." + keyColumn1 + " AND [t0]." + keyColumn2 + " = CT." + keyColumn2 + " AND " + sysOpNotDel; return getChangedItems(dc, query, tableName, lastVersion, join); } static IEnumerable getChangedItems(DataContext dc, IQueryable query, string tableName, long lastVersion, string join) { var comm = dc.GetCommand(query); var sql = setJoin(comm.CommandText, join); return dc.ExecuteQuery(sql, comm.Parameters.OfType().Select(p => p.Value).ToArray()); } const string sysOpNotDel = " SYS_CHANGE_OPERATION != 'D' "; static string getChangeTable(string tableName, long lastVersion) { return " CHANGETABLE (CHANGES " + tableName + ", " + lastVersion + ") AS CT "; } static readonly char[] newChar = new[] { '\n' }; static string setJoin(string command, string innerJoin) { var lines = command.Split(newChar); int fromIndex = -1; for (int i = 0; i < lines.Length; i++) { if (lines[i].StartsWith("FROM ")) { if (fromIndex != -1) throw new ArgumentException("Multiple FROM lines found in SQL command."); fromIndex = i; lines[i] += innerJoin; } } if (fromIndex == -1) throw new ArgumentException("No FROM line found in SQL command."); return string.Join("\n", lines); } /* Example SQL generated from a simple command: SELECT [t0].[RatePlanId], [t0].[Prefix], [t0].[InitialIntervalExpr], [t0].[PeriodicIntervalExpr], [t0].[ConnectionChargeExpr], [t0].[RateExpr] FROM [dbo].[RateItems] AS [t0] WHERE [t0].[RatePlanId] = @p0 ORDER BY [t0].[RatePlanId] * * We may have to insert the where ourselves if one is not found (insert it on the line after FROM) SELECT [t0].[RatePlanId], [t0].[Prefix], [t0].[InitialIntervalExpr], [t0].[PeriodicIntervalExpr], [t0].[ConnectionChargeExpr], [t0].[RateExpr] FROM [dbo].[RateItems] AS [t0] ORDER BY [t0].[RatePlanId] * * More advanced (LoadWith): * SELECT [t0].[EndpointName], [t0].[EndpointSetName], [t0].[WeightOrPriority], [t0].[InboundCallLimit], [t0].[KeepAliveInterval], [t0].[KeepAliveTimeout], [t0].[ escription], [t0].[Created], [t0].[ProtocolOptions], [t2].[test], [t2].[EndpointName] AS [EndpointName2], [t2].[IPs], [t2].[OriginalIPHeader], [t2].[UserName], [t2].[Password], [t2].[FromDialedNumberPrefix], [t2].[FromDialedNumberPrefixAutoRemove], [t2].[DialedNumberPrefix], [t2].[DialedNumberPrefixAutoRemove], [t2].[ ustomHeaderName], [t2].[CustomHeaderValue], [t2].[Certificate], [t4].[test] AS [test2], [t4].[EndpointName] AS [EndpointName3], [t4].[Host], [t4].[OutboundProx ], [t4].[UserName] AS [UserName2], [t4].[Password] AS [Password2], [t4].[DialStringPrefix], [t4].[FromDialStringPrefix], [t4].[CustomHeaderName] AS [CustomHead rName2], [t4].[CustomHeaderValue] AS [CustomHeaderValue2], [t4].[Certificate] AS [Certificate2] FROM [dbo].[Endpoints] AS [t0] LEFT OUTER JOIN ( SELECT 1 AS [test], [t1].[EndpointName], [t1].[IPs], [t1].[OriginalIPHeader], [t1].[UserName], [t1].[Password], [t1].[FromDialedNumberPrefix], [t1].[FromDi ledNumberPrefixAutoRemove], [t1].[DialedNumberPrefix], [t1].[DialedNumberPrefixAutoRemove], [t1].[CustomHeaderName], [t1].[CustomHeaderValue], [t1].[Certificat ] FROM [dbo].[EndpointAuthentication] AS [t1] ) AS [t2] ON [t2].[EndpointName] = [t0].[EndpointName] LEFT OUTER JOIN ( SELECT 1 AS [test], [t3].[EndpointName], [t3].[Host], [t3].[OutboundProxy], [t3].[UserName], [t3].[Password], [t3].[DialStringPrefix], [t3].[FromDialString refix], [t3].[CustomHeaderName], [t3].[CustomHeaderValue], [t3].[Certificate] FROM [dbo].[EndpointConnection] AS [t3] ) AS [t4] ON [t4].[EndpointName] = [t0].[EndpointName] Notice that sub-clauses have spaces first. */ #endregion #region Keys static string changeOpToSql(ChangeAction a) { switch (a) { case ChangeAction.Delete: return "'D'"; case ChangeAction.Insert: return "'I'"; case ChangeAction.Update: return "'U'"; default: throw new ArgumentException("Invalid ChangeAction."); } } const string whereOp = "WHERE SYS_CHANGE_OPERATION = "; public static IEnumerable GetChangedKeys(DataContext dc, string tableName, string keyColumn, long lastVersion, ChangeAction changeAction) { return GetChangedKeys(dc, tableName, keyColumn, lastVersion, changeAction, null); } public static IEnumerable GetChangedKeys(DataContext dc, string tableName, string keyColumn, long lastVersion, ChangeAction changeAction, string filterString) { if (!string.IsNullOrEmpty(filterString)) filterString = " AND " + filterString; string sql = "SELECT [" + keyColumn + "] FROM " + getChangeTable(tableName, lastVersion) + whereOp + changeOpToSql(changeAction) + " " + filterString; return dc.ExecuteQuery(sql); } public static IEnumerable> GetChangedKeys(DataContext dc, string tableName, string keyColumn1, string keyColumn2, long lastVersion, ChangeAction changeAction) { string sql = "SELECT [" + keyColumn1 + "] Key1, [" + keyColumn2 + "] Key2 FROM " + getChangeTable(tableName, lastVersion) + whereOp + changeOpToSql(changeAction); return dc.ExecuteQuery>(sql) .Select(t => Tuple.Create(t.Key1, t.Key2)); } public static IEnumerable> GetKeys(DataContext dc, string sqlCommand, params object[] parameters) { return dc.ExecuteQuery>(sqlCommand, parameters) .Select(t => Tuple.Create(t.Key1, t.Key2)); } /// Interop class to get values out of two-column query. Apparently structs don't work. class TwoKeyHolder { public Ta Key1 { get; set; } public Tb Key2 { get; set; } } #endregion } }