2wn


SUBMITTED BY: 2wn

DATE: May 27, 2022, 11:10 p.m.

FORMAT: Text only

SIZE: 8.9 kB

HITS: 286

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Data;
  4. using System.Data.SqlClient;
  5. using System.Linq;
  6. using System.Text;
  7. using Microsoft.SqlServer.Server;
  8. namespace CodeGenerator
  9. {
  10. /// <summary>
  11. /// A tool for generating unique random codes.
  12. /// </summary>
  13. /// <example>
  14. /// <code>
  15. /// using (var connection = new SqlConnection(ConnectionString))
  16. /// {
  17. /// connection.Open();
  18. /// var codeLength = GetCurrentCodeLength(connection);
  19. /// using (var generator = new CodeGenerator(connection, codeLength))
  20. /// {
  21. /// var codes = generator.GenerateCodes(10000);
  22. /// foreach(var code in codes)
  23. /// Console.WriteLine(code);
  24. /// if (generator.CodeLength > codeLength)
  25. /// {
  26. /// SaveNewCodeLength(generator.CodeLength);
  27. /// NotifyDeveloperOfApproachingCodePoolExhaustion(
  28. /// generator.CodeLength,
  29. /// CodeGenerator.MaxCodeLength);
  30. /// }
  31. /// }
  32. /// }
  33. /// </code>
  34. /// </example>
  35. public class CodeGenerator : IDisposable
  36. {
  37. public const int MaxCodeLength = 8;
  38. private const string AvailableChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  39. private const string Query = @"
  40. DECLARE @batchid uniqueidentifier;
  41. SET @batchid = NEWID();
  42. INSERT INTO dbo.Voucher (Code, BatchId)
  43. SELECT DISTINCT b.Code, @batchid
  44. FROM @batch b
  45. WHERE NOT EXISTS (
  46. SELECT Code
  47. FROM dbo.Voucher v
  48. WHERE b.Code = v.Code
  49. );
  50. SELECT Code
  51. FROM dbo.Voucher
  52. WHERE BatchId = @batchid;";
  53. private static readonly SqlMetaData[] BatchMetaData = new[]
  54. {
  55. new SqlMetaData("Code", SqlDbType.NVarChar, MaxCodeLength)
  56. };
  57. private readonly SqlConnection _connection;
  58. private readonly StringBuilder _builder;
  59. private readonly Random _random = new Random(Guid.NewGuid().GetHashCode());
  60. private readonly int _batchSize;
  61. private readonly double _collisionThreshold;
  62. private readonly SqlCommand _command;
  63. private readonly SqlParameter _batchParameter;
  64. private bool _disposed;
  65. public int CodeLength { get; private set; }
  66. /// <summary>
  67. /// Create a CodeGenerator instance.
  68. /// </summary>
  69. /// <param name="connection">
  70. /// The connection to the database. Must be open. Calling code is responsible for
  71. /// creating, opening, and disposing the connection.
  72. /// </param>
  73. /// <param name="codeLength">
  74. /// The initial code length, which will grow as needed as codes are used up. However, you
  75. /// still need to persist the <c>CodeLength</c> property value and initialize this parameter
  76. /// correctly. Otherwise, if you always supply the same initial
  77. /// <paramref name="codeLength"/> - say "4", then ALL of the 4-digit codes will eventually
  78. /// become used up instead of maintaining the sparseness dictated by the collision
  79. /// threshold.
  80. /// </param>
  81. public CodeGenerator(SqlConnection connection, int codeLength)
  82. : this(connection, codeLength, 500, 0.01)
  83. { }
  84. /// <summary>
  85. /// Create a CodeGenerator instance.
  86. /// </summary>
  87. /// <param name="connection">
  88. /// The connection to the database. Must be open. Calling code is responsible for
  89. /// creating, opening, and disposing the connection.
  90. /// </param>
  91. /// <param name="codeLength">
  92. /// The initial code length, which will grow as needed as codes are used up. However, you
  93. /// still need to persist the <c>CodeLength</c> property value and initialize this parameter
  94. /// correctly. Otherwise, if you always supply the same initial
  95. /// <paramref name="codeLength"/> - say "4", then ALL of the 4-digit codes will eventually
  96. /// become used up instead of maintaining the sparseness dictated by the
  97. /// <paramref name="collisionThreshold"/> parameter.
  98. /// </param>
  99. /// <param name="batchSize">
  100. /// The number of codes to generate, test, and insert at once. Tune this value for best
  101. /// performance. In my tests, 500 worked well.
  102. /// </param>
  103. /// <param name="collisionThreshold">
  104. /// A value between 0 (inclusive) and 1 (exclusive). Supply a small value (perhaps 0.01) to
  105. /// keep codes sparse. A value that is too high (above 0.5) will result in sub-optimum
  106. /// performance.
  107. /// </param>
  108. public CodeGenerator(SqlConnection connection, int codeLength, int batchSize, double collisionThreshold)
  109. {
  110. if (collisionThreshold >= 1.0)
  111. throw new ArgumentOutOfRangeException("collisionThreshold", collisionThreshold, "must be less than 1");
  112. _connection = connection;
  113. CodeLength = codeLength;
  114. _batchSize = batchSize;
  115. _collisionThreshold = collisionThreshold;
  116. _builder = new StringBuilder(codeLength + 1);
  117. _command = _connection.CreateCommand();
  118. _command.CommandText = Query;
  119. _batchParameter = _command.Parameters.Add("@batch", SqlDbType.Structured);
  120. _batchParameter.TypeName = "dbo.VoucherCodeList";
  121. }
  122. public void Dispose()
  123. {
  124. if (_disposed)
  125. return;
  126. _command.Dispose();
  127. _disposed = true;
  128. }
  129. /// <summary>
  130. /// Generates unique random codes and inserts them into the database.
  131. /// </summary>
  132. /// <param name="numberOfCodes">The number of codes you need.</param>
  133. /// <returns>A list of unique random codes.</returns>
  134. public ICollection<string> GenerateCodes(int numberOfCodes)
  135. {
  136. var result = new List<string>(numberOfCodes);
  137. while (result.Count < numberOfCodes)
  138. {
  139. var batchSize = Math.Min(_batchSize, numberOfCodes - result.Count);
  140. var batch = GetBatch(batchSize);
  141. var oldResultCount = result.Count;
  142. result.AddRange(FilterAndSecureBatch(batch));
  143. var filteredBatchSize = result.Count - oldResultCount;
  144. var collisionRatio = ((double)batchSize - filteredBatchSize) / batchSize;
  145. if (collisionRatio > _collisionThreshold)
  146. CodeLength++;
  147. }
  148. return result;
  149. }
  150. private IEnumerable<string> GetBatch(int batchSize)
  151. {
  152. for (var i = 0; i < batchSize; i++)
  153. yield return GenerateRandomCode();
  154. }
  155. private string GenerateRandomCode()
  156. {
  157. _builder.Clear();
  158. for (var i = 0; i < CodeLength; i++)
  159. _builder.Append(AvailableChars[_random.Next(AvailableChars.Length)]);
  160. return _builder.ToString();
  161. }
  162. private IEnumerable<string> FilterAndSecureBatch(IEnumerable<string> batch)
  163. {
  164. _batchParameter.Value = batch.Select(x =>
  165. {
  166. var record = new SqlDataRecord(BatchMetaData);
  167. record.SetString(0, x);
  168. return record;
  169. });
  170. using (var reader = _command.ExecuteReader())
  171. while (reader.Read())
  172. yield return reader.GetString(0);
  173. }
  174. /// <summary>
  175. /// Creates the database schema required by the CodeCenerator.
  176. /// </summary>
  177. /// <param name="connection">An open connection to the database.</param>
  178. public static void CreateSchema(SqlConnection connection)
  179. {
  180. using (var command = connection.CreateCommand())
  181. {
  182. command.CommandText = @"
  183. CREATE TABLE dbo.Voucher (
  184. Code nvarchar(" + MaxCodeLength + @") COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL PRIMARY KEY,
  185. BatchId uniqueidentifier NOT NULL
  186. );
  187. CREATE NONCLUSTERED INDEX IX_Voucher ON dbo.Voucher (BatchId ASC);
  188. CREATE TYPE dbo.VoucherCodeList AS TABLE (
  189. Code nvarchar(" + MaxCodeLength + @") COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL
  190. );";
  191. command.ExecuteNonQuery();
  192. }
  193. }
  194. }
  195. }

comments powered by Disqus