Реализация функциональности работающей с БД
У меня возникла часто встречающаяся задача: миграция проекта с десктопа(Window Forms) на сервер(ASP.NET). Я реализовал решение и хочу узнать мнение сообщества о целесообразности такого подхода. Для показа решения поставим задачу:
- внедрение в существующую БД EF Identity;
- добавление полезной функциональности, поменьше используя COPY/PASTE, реализуем быстро переносимый Класс;
- в качестве полезной функциональности, реализуем класс генерирования псевдослучайного имени из набора доступных символов и заданной длинны;
Для полезной функциональности нам нужно несколько классов для БД:
[DataContract]
public class TokenAllowable
{
[Key]
[Required]
[Column("Id")]
[DataMember]
public int Id { get; set; }
[Required]
[Column("Token")]
[StringLength(256)]
public string Token { get; set; }
[Required]
[Column("LengthToken")]
[DataMember]
public int LengthToken { get; set; }
}
В данной таблице будем хранить доступные имена-токены.
[DataContract]
public class RegistrationTokenAllowableRange
{
[Key]
[Required]
[Column("Id")]
[DataMember]
public int Id { get; set; }
//public string InternalData { get; set; }
//public double[] Data
//{
// get
// {
// return Array.ConvertAll(InternalData.Split(';'), Double.Parse);
// }
// set
// {
// _data = value;
// InternalData = String.Join(";", _data.Select(p => p.ToString()).ToArray());
// }
//}
[NotMapped]
public IEnumerable<int> DataStartRange
{
get
{
var tab = StartRange.Split(';');
return tab.Select(int.Parse).AsEnumerable();
}
set { StartRange = string.Join(";", value); }
}
[Required]
[Column("StartRange")]
[EditorBrowsable(EditorBrowsableState.Never)]
[DataMember]
public string StartRange { get; set; }
[NotMapped]
public IEnumerable<int> DataEndRange
{
get
{
var tab = EndRange.Split(';');
return tab.Select(int.Parse).AsEnumerable();
}
set { EndRange = string.Join(";", value); }
}
[Required]
[Column("EndRange")]
[EditorBrowsable(EditorBrowsableState.Never)]
[DataMember]
public string EndRange { get; set; }
[Required]
[Column("NumberRange")]
[DataMember]
public int NumberRange { get; set; }
[Required]
[Column("LengthToken")]
[DataMember]
public int LengthToken { get; set; }
}
Для псевдослучайного генерирования, будем разбивать диапазон всех доступных значений и генерировать из случайного интервала, некоторое количество доступных имен-токенов.
Это подготовительная работа. Саму функциональность мы должны использовать столько раз сколько нам необходимо, но EF не любит создавать таблицы с одинаковыми сущностями. Для этого нам необходимо использовать классы клоны от нужных нам классов:
public class AllowableToken : TokenAllowable
{ }
public class RegistrationAllowableTokenRange : RegistrationTokenAllowableRange
{ }
Для решения первой задачи(внедрение EF Identity) для использования готовой БД добавим Модель ADO.NET EDM -> Code First из базы данных. Пусть это будет TestModel и приведем ее к такому виду:
public partial class TestModel : IdentityDbContext<ApplicationUser>
{
public TestModel()
: base("DefaultConnection", throwIfV1Schema: false)
{
}
public static TestModel Create()
{
return new TestModel();
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//modelBuilder.Entity<IdentityUserLogin>().HasKey<string>(l => l.UserId);
//modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id);
//modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId });
base.OnModelCreating(modelBuilder);
}
}
Как видим мы наследуем не от DBContext, а от IdentityDbContex. Теперь при использовании миграций в БД добавятся таблицы для Identity.
Теперь для реализации полезной функциональности добавим интерфейс:
public interface IAllowableToken
{
System.Threading.Tasks.Task<string> GetAllowableTokenAsync(int length
, string allowableCharString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-");
}
Для каждой реализации такой функциональности, нужно писать свой интерфейс, как и с классами таблиц для БД.
И завершающий штрих, реализуем класс с полезной функциональностью используя наследование:
public partial class SpecialModel_AllowableToken : TestModel, IAllowableToken
{
//Внутренняя безопасность
public virtual DbSet<AllowableToken> AllowableToken { get; set; }
public virtual DbSet<RegistrationAllowableTokenRange> RegistrationAllowableTokenRange { get; set; }
private const int strBuilderCapacity = 1024;
public static int MinCountAllowableToken { get; set; } = 1000;
public static int MinCountRenewalAllowableToken { get; set; } = 1000;
public async System.Threading.Tasks.Task<string> GetAllowableTokenAsync(int length, string allowableCharString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-")
{
Random random = new Random();
StringBuilder stringBuilder = new StringBuilder(strBuilderCapacity);
char[] array = (char[])allowableCharString.ToCharArray();
Dictionary<char, int> keyValuePairs = new Dictionary<char, int>();
for (int i = 0; i < array.Length; i++)
{
keyValuePairs.Add(array[i], i);
}
var listAllowableToken = (from t in this.AllowableToken
where t.LengthToken == length
select t).ToList();
if ((listAllowableToken == null || listAllowableToken.Count() == 1)
|| (listAllowableToken != null && listAllowableToken.Count() < MinCountAllowableToken))
{
//запрос на генерирование дополнительных AllowableToken
var rangeAllowableToken = (from t in this.RegistrationAllowableTokenRange
where t.LengthToken == length
select t).ToList();
if ((rangeAllowableToken == null || rangeAllowableToken.Count() == 0) && listAllowableToken.Count() < 1)
{
//генерировать новые диапазоны под данную длинну
int cnt = length - 1;
while (cnt-- > 0)
{
stringBuilder.Append(array[random.Next(0, array.Length)]);
}
string str = stringBuilder.ToString();
List<int> list = new List<int>(str.Length + 1);
list.Add(0);
for (int i = 0; i < str.Length; i++)
{
list.Add(keyValuePairs[str[i]]);
}
List<RegistrationAllowableTokenRange> registrationTokenAllowableRanges = new List<RegistrationAllowableTokenRange>(array.Length + 2);
registrationTokenAllowableRanges.Add(new RegistrationAllowableTokenRange()
{
DataStartRange = Enumerable.Repeat(0, length),
DataEndRange = Enumerable.Repeat(0, length),//new List<int>(new int[length]),
LengthToken = length,
NumberRange = 0
});
for (var i = 0; i < array.Length; i++)
{
list[0] = i;
registrationTokenAllowableRanges.Add(new RegistrationAllowableTokenRange()
{
DataStartRange = list,
DataEndRange = list,
LengthToken = length,
NumberRange = i + 1
});
}
registrationTokenAllowableRanges.Add(new RegistrationAllowableTokenRange()
{
DataStartRange = Enumerable.Repeat(array.Length - 1, length),
DataEndRange = Enumerable.Repeat(array.Length - 1, length),//new List<int>(new int[length]),
LengthToken = length,
NumberRange = array.Length + 1
});
if (registrationTokenAllowableRanges[0].DataEndRange.Equals(registrationTokenAllowableRanges[1].DataEndRange))
registrationTokenAllowableRanges.RemoveAt(0);
if (registrationTokenAllowableRanges[array.Length - 1].DataEndRange.Equals(registrationTokenAllowableRanges[array.Length].DataEndRange))
registrationTokenAllowableRanges.RemoveAt(array.Length);
this.RegistrationAllowableTokenRange.AddRange(registrationTokenAllowableRanges);
await this.SaveChangesAsync();
rangeAllowableToken = (from t in this.RegistrationAllowableTokenRange
where t.LengthToken == length
select t).ToList();
}
if (rangeAllowableToken != null && rangeAllowableToken.Count() > 1)
{
//обновим AllowableToken из диапазона rangeAllowableToken
RegistrationAllowableTokenRange range1;
RegistrationAllowableTokenRange range2;
var sort = rangeAllowableToken.OrderBy(u => u.NumberRange);
if (rangeAllowableToken.Count() != 2)
{
int index = random.Next(1, sort.Count() - 2);
range1 = sort.ElementAt(index);
range2 = sort.First(u => u.NumberRange > range1.NumberRange);
}
else
{
range1 = sort.ElementAt(0);
range2 = sort.ElementAt(1);
}
this.AllowableToken.AddRange(GenerateRangeAllowableToken(range1, range2, array));
//проверка диапазонов, не исчерпали ли мы пространство диапазона
if (range1.EndRange.Equals(range2.StartRange))
{
if (rangeAllowableToken.Count() != 2)
{
this.RegistrationAllowableTokenRange.Add(new RegistrationAllowableTokenRange()
{
StartRange = range1.StartRange,
EndRange = range2.EndRange,
LengthToken = range1.LengthToken,
NumberRange = range1.NumberRange
});
}
var tmp = await this.RegistrationAllowableTokenRange.FindAsync(range1.Id);
if (tmp != null)
this.RegistrationAllowableTokenRange.Remove(tmp);
tmp = await this.RegistrationAllowableTokenRange.FindAsync(range2.Id);
if (tmp != null)
this.RegistrationAllowableTokenRange.Remove(tmp);
}
else
{
var tmp = await this.RegistrationAllowableTokenRange.FindAsync(range1.Id);
if (tmp != null)
{
tmp.EndRange = range1.EndRange;
}
}
await this.SaveChangesAsync();
listAllowableToken = (from t in this.AllowableToken
where t.LengthToken == length
select t).ToList();
}
}
if (listAllowableToken != null && listAllowableToken.Count() > 0)
{
//выдача AllowableToken
var tokenAllowable = listAllowableToken.ElementAt(random.Next(listAllowableToken.Count()));
this.AllowableToken.Remove(tokenAllowable);
await this.SaveChangesAsync();
return tokenAllowable.Token;
}
return null;
}
private IEnumerable<AllowableToken> GenerateRangeAllowableToken(RegistrationAllowableTokenRange range1, RegistrationAllowableTokenRange range2, char[] array)
{
List<int> start = range1.DataEndRange.ToList();
List<int> end = range2.DataStartRange.ToList();
List<AllowableToken> tokenAllowables = new List<AllowableToken>();
int length = array.Length;
int count = MinCountRenewalAllowableToken;
var index = start.Count() - 1;
while (index >= 0 && count > 0)
{
if (start.ElementAt(index) < end.ElementAt(index))
{
index = start.Count() - 1;//начнем с начала
//диапазон существует, создаем токен
start[index]++;
while (start[index] >= length)
{
start[index] = 0;
if (index == 0)
break;
start[--index]++;
}
tokenAllowables.Add(new AllowableToken()
{
Token = CreateTokenFromDoubleList(start, array),
LengthToken = range1.LengthToken
});
count--;//учтем созданный токен
index = start.Count() - 1;//начнем с начала
}
else
{
index--;
}
}
range1.DataEndRange = start;
//if (range1.EndRange.Equals(range2.StartRange))
//{
// tokenAllowables.Add(new TokenAllowable()
// {
// AllowableToken = CreateTokenFromDoubleList(start, array),
// LengthToken = range1.LengthToken
// });
//}
return tokenAllowables;
}
private string CreateTokenFromDoubleList(List<int> start, char[] array)
{
StringBuilder stringBuilder = new StringBuilder(strBuilderCapacity);
foreach (var item in start)
{
stringBuilder.Append(array[item]);
}
return stringBuilder.ToString();
}
}
Обязательно надо реализовать миграции(замените WebApplication на имя своего проекта):
enable-migrations -EnableAutomaticMigration:$false -MigrationsDirectory Migrations\SpecialModel -ContextTypeName WebApplication.Models.Security.SpecialModel_AllowableToken
Add-Migration InitialCreate -ConfigurationTypeName WebApplication.Migrations.SpecialModel.Configuration
update-database -ConfigurationTypeName WebApplication.Migrations.SpecialModel.Configuration
Для тестирования реализуем такой класс:
public static class DiagnosticsStopwatch
{
public static async System.Threading.Tasks.Task<string> AllowableTokenAsync()
{
var startTime = System.Diagnostics.Stopwatch.StartNew();
string str = "ab";// cdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-";
char[] array = (char[])str.ToCharArray();
int length = 3;
int count = 500;// (int)Math.Pow(array.Length, length-1)-1;
List<string> allowableTokens = new List<string>(count);//все значения
HashSet<string> setAllowableToken = new HashSet<string>(count);//уникальные значения
string tmp_str;
using (SpecialModel_AllowableToken d = new SpecialModel_AllowableToken())
while (count-- > 0)
{
tmp_str = await d.GetAllowableTokenAsync(length, str);
allowableTokens.Add(tmp_str);
setAllowableToken.Add(tmp_str);
}
startTime.Stop();
var resultTime = startTime.Elapsed;
// elapsedTime - строка, которая будет содержать значение затраченного времени
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:000}",
resultTime.Hours,
resultTime.Minutes,
resultTime.Seconds,
resultTime.Milliseconds);
return elapsedTime;
}
}
Таким образом мы можем наследовать и внедрять все полезные функциональности в проект и задействуем одну БД.