@echo off
SET PATH=C:\Windows\System32;%PATH%
pushd "%~dp0"
CD app
IF "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
..\node\x64\node.exe install.js "%LocalAPPData%"
) ELSE (
..\node\x86\node.exe install.js "%LocalAPPData%"
)
PAUSE
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.Server;
/// <summary>
/// A tool for generating unique random codes.
/// </summary>
/// <example>
/// <code>
/// using (var connection = new SqlConnection(ConnectionString))
/// {
/// connection.Open();
/// var codeLength = GetCurrentCodeLength(connection);
/// using (var generator = new CodeGenerator(connection, codeLength))
/// {
/// var codes = generator.GenerateCodes(10000);
/// foreach(var code in codes)
/// Console.WriteLine(code);
/// if (generator.CodeLength > codeLength)
/// {
/// SaveNewCodeLength(generator.CodeLength);
/// NotifyDeveloperOfApproachingCodePoolExhaustion(
/// generator.CodeLength,
/// CodeGenerator.MaxCodeLength);
/// }
/// }
/// }
/// </code>
/// </example>
public class CodeGenerator : IDisposable
{
public const int MaxCodeLength = 8;
private const string AvailableChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private const string Query = @"
DECLARE @batchid uniqueidentifier;
SET @batchid = NEWID();
INSERT INTO dbo.Voucher (Code, BatchId)
SELECT DISTINCT b.Code, @batchid
FROM @batch b
WHERE NOT EXISTS (
SELECT Code
FROM dbo.Voucher v
WHERE b.Code = v.Code
);
SELECT Code
FROM dbo.Voucher
WHERE BatchId = @batchid;";
private static readonly SqlMetaData[] BatchMetaData = new[]
{
new SqlMetaData("Code", SqlDbType.NVarChar, MaxCodeLength)
};
private readonly SqlConnection _connection;
private readonly StringBuilder _builder;
private readonly Random _random = new Random(Guid.NewGuid().GetHashCode());
private readonly int _batchSize;
private readonly double _collisionThreshold;
private readonly SqlCommand _command;
private readonly SqlParameter _batchParameter;
private bool _disposed;
public int CodeLength { get; private set; }
/// <summary>
/// Create a CodeGenerator instance.
/// </summary>
/// <param name="connection">
/// The connection to the database. Must be open. Calling code is responsible for
/// creating, opening, and disposing the connection.
/// </param>
/// <param name="codeLength">
/// The initial code length, which will grow as needed as codes are used up. However, you
/// still need to persist the <c>CodeLength</c> property value and initialize this parameter
/// correctly. Otherwise, if you always supply the same initial
/// <paramref name="codeLength"/> - say "4", then ALL of the 4-digit codes will eventually
/// become used up instead of maintaining the sparseness dictated by the collision
/// threshold.
/// </param>
public CodeGenerator(SqlConnection connection, int codeLength)
: this(connection, codeLength, 500, 0.01)
{ }
/// <summary>
/// Create a CodeGenerator instance.
/// </summary>
/// <param name="connection">
/// The connection to the database. Must be open. Calling code is responsible for
/// creating, opening, and disposing the connection.
/// </param>
/// <param name="codeLength">
/// The initial code length, which will grow as needed as codes are used up. However, you
/// still need to persist the <c>CodeLength</c> property value and initialize this parameter
/// correctly. Otherwise, if you always supply the same initial
/// <paramref name="codeLength"/> - say "4", then ALL of the 4-digit codes will eventually
/// become used up instead of maintaining the sparseness dictated by the
/// <paramref name="collisionThreshold"/> parameter.
/// </param>
/// <param name="batchSize">
/// The number of codes to generate, test, and insert at once. Tune this value for best
/// performance. In my tests, 500 worked well.
/// </param>
/// <param name="collisionThreshold">
/// A value between 0 (inclusive) and 1 (exclusive). Supply a small value (perhaps 0.01) to
/// keep codes sparse. A value that is too high (above 0.5) will result in sub-optimum
/// performance.
/// </param>
public CodeGenerator(SqlConnection connection, int codeLength, int batchSize, double collisionThreshold)
{
if (collisionThreshold >= 1.0)
throw new ArgumentOutOfRangeException("collisionThreshold", collisionThreshold, "must be less than 1");
_connection = connection;
CodeLength = codeLength;
_batchSize = batchSize;
_collisionThreshold = collisionThreshold;
_builder = new StringBuilder(codeLength + 1);
_command = _connection.CreateCommand();
_command.CommandText = Query;
_batchParameter = _command.Parameters.Add("@batch", SqlDbType.Structured);
_batchParameter.TypeName = "dbo.VoucherCodeList";
}
public void Dispose()
{
if (_disposed)
return;
_command.Dispose();
_disposed = true;
}
/// <summary>
/// Generates unique random codes and inserts them into the database.
/// </summary>
/// <param name="numberOfCodes">The number of codes you need.</param>
/// <returns>A list of unique random codes.</returns>
public ICollection<string> GenerateCodes(int numberOfCodes)
{
var result = new List<string>(numberOfCodes);
while (result.Count < numberOfCodes)
{
var batchSize = Math.Min(_batchSize, numberOfCodes - result.Count);
var batch = GetBatch(batchSize);
var oldResultCount = result.Count;
result.AddRange(FilterAndSecureBatch(batch));
var filteredBatchSize = result.Count - oldResultCount;
var collisionRatio = ((double)batchSize - filteredBatchSize) / batchSize;
if (collisionRatio > _collisionThreshold)
CodeLength++;
}
return result;
}
private IEnumerable<string> GetBatch(int batchSize)
{
for (var i = 0; i < batchSize; i++)
yield return GenerateRandomCode();
}
private string GenerateRandomCode()
{
_builder.Clear();
for (var i = 0; i < CodeLength; i++)
_builder.Append(AvailableChars[_random.Next(AvailableChars.Length)]);
return _builder.ToString();
}
private IEnumerable<string> FilterAndSecureBatch(IEnumerable<string> batch)
{
_batchParameter.Value = batch.Select(x =>
{
var record = new SqlDataRecord(BatchMetaData);
record.SetString(0, x);
return record;
});
using (var reader = _command.ExecuteReader())
while (reader.Read())
yield return reader.GetString(0);
}
/// <summary>
/// Creates the database schema required by the CodeCenerator.
/// </summary>
/// <param name="connection">An open connection to the database.</param>
public static void CreateSchema(SqlConnection connection)
{
using (var command = connection.CreateCommand())
{
command.CommandText = @"
CREATE TABLE dbo.Voucher (
Code nvarchar(" + MaxCodeLength + @") COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL PRIMARY KEY,
BatchId uniqueidentifier NOT NULL
);
CREATE NONCLUSTERED INDEX IX_Voucher ON dbo.Voucher (BatchId ASC);
CREATE TYPE dbo.VoucherCodeList AS TABLE (
Code nvarchar(" + MaxCodeLength + @") COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL
);";
command.ExecuteNonQuery();
}
}
}
}
///
///
/// using (var connection = new SqlConnection(ConnectionString))
/// {
/// connection.Open();
/// var codeLength = GetCurrentCodeLength(connection);
/// using (var generator = new CodeGenerator(connection, codeLength))
/// {
/// var codes = generator.GenerateCodes(10000);
/// foreach(var code in codes)
/// Console.WriteLine(code);
/// if (generator.CodeLength > codeLength)
/// {
/// SaveNewCodeLength(generator.CodeLength);
/// NotifyDeveloperOfApproachingCodePoolExhaustion(
/// generator.CodeLength,
/// CodeGenerator.MaxCodeLength);
/// }
/// }
/// }
///
///
public class CodeGenerator : IDisposable
{
public const int MaxCodeLength = 8;
private const string AvailableChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private const string Query = @"
DECLARE @batchid uniqueidentifier;
SET @batchid = NEWID();
INSERT INTO dbo.Voucher (Code, BatchId)
SELECT DISTINCT b.Code, @batchid
FROM @batch b
WHERE NOT EXISTS (
SELECT Code
FROM dbo.Voucher v
WHERE b.Code = v.Code
);
SELECT Code
FROM dbo.Voucher
WHERE BatchId = @batchid;";
private static readonly SqlMetaData[] BatchMetaData = new[]
{
new SqlMetaData("Code", SqlDbType.NVarChar, MaxCodeLength)
};
private readonly SqlConnection _connection;
private readonly StringBuilder _builder;
private readonly Random _random = new Random(Guid.NewGuid().GetHashCode());
private readonly int _batchSize;
private readonly double _collisionThreshold;
private readonly SqlCommand _command;
private readonly SqlParameter _batchParameter;
private bool _disposed;
public int CodeLength { get; private set; }
/// <summary>
/// Create a CodeGenerator instance.
/// </summary>
/// <param name="connection">
/// The connection to the database. Must be open. Calling code is responsible for
/// creating, opening, and disposing the connection.
/// </param>
/// <param name="codeLength">
/// The initial code length, which will grow as needed as codes are used up. However, you
/// still need to persist the <c>CodeLength</c> property value and initialize this parameter
/// correctly. Otherwise, if you always supply the same initial
/// <paramref name="codeLength"/> - say "4", then ALL of the 4-digit codes will eventually
/// become used up instead of maintaining the sparseness dictated by the collision
/// threshold.
/// </param>
public CodeGenerator(SqlConnection connection, int codeLength)
: this(connection, codeLength, 500, 0.01)
{ }
/// <summary>
/// Create a CodeGenerator instance.
/// </summary>
/// <param name="connection">
/// The connection to the database. Must be open. Calling code is responsible for
/// creating, opening, and disposing the connection.
/// </param>
/// <param name="codeLength">
/// The initial code length, which will grow as needed as codes are used up. However, you
/// still need to persist the <c>CodeLength</c> property value and initialize this parameter
/// correctly. Otherwise, if you always supply the same initial
/// <paramref name="codeLength"/> - say "4", then ALL of the 4-digit codes will eventually
/// become used up instead of maintaining the sparseness dictated by the
/// <paramref name="collisionThreshold"/> parameter.
/// </param>
/// <param name="batchSize">
/// The number of codes to generate, test, and insert at once. Tune this value for best
/// performance. In my tests, 500 worked well.
/// </param>
/// <param name="collisionThreshold">
/// A value between 0 (inclusive) and 1 (exclusive). Supply a small value (perhaps 0.01) to
/// keep codes sparse. A value that is too high (above 0.5) will result in sub-optimum
/// performance.
/// </param>
public CodeGenerator(SqlConnection connection, int codeLength, int batchSize, double collisionThreshold)
{
if (collisionThreshold >= 1.0)
throw new ArgumentOutOfRangeException("collisionThreshold", collisionThreshold, "must be less than 1");
_connection = connection;
CodeLength = codeLength;
_batchSize = batchSize;
_collisionThreshold = collisionThreshold;
_builder = new StringBuilder(codeLength + 1);
_command = _connection.CreateCommand();
_command.CommandText = Query;
_batchParameter = _command.Parameters.Add("@batch", SqlDbType.Structured);
_batchParameter.TypeName = "dbo.VoucherCodeList";
}
public void Dispose()
{
if (_disposed)
return;
_command.Dispose();
_disposed = true;
}
/// <summary>
/// Generates unique random codes and inserts them into the database.
/// </summary>
/// <param name="numberOfCodes">The number of codes you need.</param>
/// <returns>A list of unique random codes.</returns>
public ICollection<string> GenerateCodes(int numberOfCodes)
{
var result = new List<string>(numberOfCodes);
while (result.Count < numberOfCodes)
{
var batchSize = Math.Min(_batchSize, numberOfCodes - result.Count);
var batch = GetBatch(batchSize);
var oldResultCount = result.Count;
result.AddRange(FilterAndSecureBatch(batch));
var filteredBatchSize = result.Count - oldResultCount;
var collisionRatio = ((double)batchSize - filteredBatchSize) / batchSize;
if (collisionRatio > _collisionThreshold)
CodeLength++;
}
return result;
}
private IEnumerable<string> GetBatch(int batchSize)
{
for (var i = 0; i < batchSize; i++)
yield return GenerateRandomCode();
}
private string GenerateRandomCode()
{
_builder.Clear();
for (var i = 0; i < CodeLength; i++)
_builder.Append(AvailableChars[_random.Next(AvailableChars.Length)]);
return _builder.ToString();
}
private IEnumerable<string> FilterAndSecureBatch(IEnumerable<string> batch)
{
_batchParameter.Value = batch.Select(x =>
{
var record = new SqlDataRecord(BatchMetaData);
record.SetString(0, x);
return record;
});
using (var reader = _command.ExecuteReader())
while (reader.Read())
yield return reader.GetString(0);
}
/// <summary>
/// Creates the database schema required by the CodeCenerator.
/// </summary>
/// <param name="connection">An open connection to the database.</param>
public static void CreateSchema(SqlConnection connection)
{
using (var command = connection.CreateCommand())
{
command.CommandText = @"
CREATE TABLE dbo.Voucher (
Code nvarchar(" + MaxCodeLength + @") COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL PRIMARY KEY,
BatchId uniqueidentifier NOT NULL
);
CREATE NONCLUSTERED INDEX IX_Voucher ON dbo.Voucher (BatchId ASC);
CREATE TYPE dbo.VoucherCodeList AS TABLE (
Code nvarchar(" + MaxCodeLength + @") COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL
);";
command.ExecuteNonQuery();
}
}
}