Реализация функциональности работающей с БД

У меня возникла часто встречающаяся задача: миграция проекта с десктопа(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;
        }
    }

Таким образом мы можем наследовать и внедрять все полезные функциональности в проект и задействуем одну БД.


Ответы (0 шт):