Как сгенерировать пользовательский номер ключа с помощью C# в ASP.NET Core

это мой класс,

public class PatReg
{
    [DatabaseGenerated(DatabaseGeneratedOption.Computed), ScaffoldColumn(false)]
    public Int64 RecId { get; set; }
    [Key,Display(Name = "File Id"), ScaffoldColumn(true), DatabaseGenerated(DatabaseGeneratedOption.None )]
    public Int64 FileId { get; set; }
    [Required, Display(Name = "First Name")]
    public string FName { get; set; }
    [Required, Display(Name = "Middle Name")]
    public string MName { get; set; }
    [Required, Display(Name = "Last Name")]
    public string LName { get; set; }
    [Required, Display(Name = "Date of Birth"), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    public DateTime Dob { get; set; }
  }

«FileId» — это мой первичный ключ, и я хочу сгенерировать его при сохранении записи, сохранив его вместе с записью,

Пользовательский номер будет иметь следующую спецификацию: ГГММДД0001, где ГГ — две цифры года, ММ — две цифры месяца. DD — две цифры дня, 001 — серийный запуск и сброс каждый день.

это мой контроллер

// POST: PatReg/Create
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create([Bind("FileId,FName,MName,LName,Dob")] PatReg patReg)
    {

        if (ModelState.IsValid)
        {
            _context.Add(patReg);
            await _context.SaveChangesAsync();
            return RedirectToAction("Index");
        }
        return View(patReg);
    }

Фон

Раньше я генерировал этот номер с помощью хранимой процедуры SQL, как показано ниже,

 Set @YY = (RIGHT(CONVERT(VARCHAR(8), GETDATE(), 1),2))
    Set @MM =  SUBSTRING(CONVERT(nvarchar(6),getdate(), 112),5,2)
    Set @DD =  RIGHT('00' + CONVERT(NVARCHAR(2), DATEPART(DAY, GETDATE())), 2)
    Set @SRL = (SELECT FileNumSrl FROM SetTblSrls WHERE RecID = 1)
    SET @FileId =(select CAST(CONCAT ( @YY , @MM , @DD,00 ,@SRL) AS int))

«@SRL» представляет собой последовательную последовательность, которую я ищу из «SetTblSrls», и у меня был триггер в целевой таблице для обновления этого числа при каждой вставке, с помощью которого я получаю новый номер каждый раз, когда я генерирую FileId

Как я могу это сделать с помощью EF и C#,


person JSON    schedule 03.08.2017    source источник
comment
Какова ваша базовая база данных? Oracle, Sql Server, MySQL и т. д.?   -  person fharreau    schedule 03.08.2017
comment
это сервер MS SQL   -  person JSON    schedule 03.08.2017
comment
Можете ли вы создавать задания или подобные вещи на вашем SQL Server? Если это так, вы можете, например, создать последовательность (истинная последовательность, доступная, по-видимому, с SQL Server 2012, а не идентификатор в таблице, который требует каждый раз вставлять новую строку). А затем отбросить и воссоздать эту последовательность каждый день в полночь с заданием.   -  person fharreau    schedule 03.08.2017
comment
Проверьте свободную конфигурацию и HasDefaultValueSql stackoverflow.com/questions/42036291/   -  person Andrés Robinet    schedule 03.08.2017


Ответы (3)


Вам понадобится где-то сохранить порядковый номер, чтобы вы могли безопасно увеличивать его каждый раз, когда у вас появляется новый файл. In-memory на самом деле не вариант, поскольку IIS сбрасывает свои пулы приложений каждые 29 часов по умолчанию, теряя все, что вы могли кэшировать. Таким образом, вы остаетесь с базой данных или файловой системой.

Следующий SQL предоставляет безопасный и эффективный способ получения следующего доступного порядкового номера путем простого выполнения хранимой процедуры из кода C# на стороне сервера и считывания возвращаемого значения:

create table DailySequence
(
    SequenceDate date not null primary key,
    LastSequence int not null default(0) -- Change the default if you want your first sequence to be 1
)
go

create procedure dbo.GetNextSequence as
begin
    Declare @today date = getdate()
    Declare @table table (id int)

    begin tran

    update DailySequence set LastSequence=LastSequence+1 output inserted.LastSequence into @table where SequenceDate=@today

    if (@@ROWCOUNT=0)
        insert into DailySequence(SequenceDate) 
        output inserted.LastSequence into @table 
        values (@today)

    commit

    declare @s varchar(20) 
    select @s = convert(varchar(20), id) from @table

    if (Len(@s)<4)
        set @s = Right('000' + @s, 4)

    select CONVERT(VARCHAR(10), @today, 12) + @s [Sequence]
end
go
person Pete    schedule 03.08.2017
comment
Я знаю, как сгенерировать таблицу вместе с моей миграцией, есть ли способ сгенерировать SP с помощью ASP.NET Core 1.1 EF, Code First Migrations? - person JSON; 03.08.2017
comment
Как выполнить SP с помощью LINQ в CORE EF - person JSON; 03.08.2017
comment
Пит, не могли бы вы проверить мой метод - person JSON; 04.08.2017
comment
@Pete будет ли это обрабатывать параллелизм? - person AGH; 05.02.2019
comment
Должен подойти — все операции ввода-вывода в таблице DailySequence содержатся в транзакции, поэтому несколько одновременных запросов должны обрабатываться корректно. - person Pete; 05.02.2019

Это решение основано на использовании последовательностей SQL Server (доступно начиная с SQL Server 2012 и для базы данных SQL Azure). Если вы не можете их использовать, вы можете перейти к другому ответу.


Это решение состоит в создании последовательности, которая будет автоматически вычислять FileId. Но вам нужно будет сбрасывать последовательность каждый день в полночь, чтобы добиться желаемого. Вот как вы можете создать последовательность:

Set @YY = (RIGHT(CONVERT(VARCHAR(8), GETDATE(), 1),2))
Set @MM =  SUBSTRING(CONVERT(nvarchar(6),getdate(), 112),5,2)
Set @DD =  RIGHT('00' + CONVERT(NVARCHAR(2), DATEPART(DAY, GETDATE())), 2)

DROP SEQUENCE IF EXISTS dbo.DailyFileId;
CREATE SEQUENCE dbo.DailyFileId
    START WITH CAST(CONCAT(@YY, @MM, @DD, '0001') AS int)
    INCREMENT BY 1;  
GO  

(Или что-то в этом роде, у меня нет движка SQL Server, чтобы протестировать их. Пожалуйста, не стесняйтесь исправить их в комментариях, если это необходимо)

Чтобы запускать скрипт каждый день, вы можете использовать агент SQL. На мой взгляд, это лучший вариант, но вы также можете запустить новый поток в своем приложении, который будет запускать скрипт каждый день.

Если вы предпочитаете этот вариант, вот как вы можете это сделать. Я позволю вам решить, где вам нужно поместить этот код в ваше приложение:

// Call that method as close as you can from the startup of your application
Task.Run(() => DailyResetSequence());

private async void DailyResetSequence()
{
    while (true)
    {
        using (var dbContext = new DbContext())
        {
            var tomorrow = DateTime.Now.AddDays(1);
            var sleepingTime = tomorrow - DateTime.Now;

            // waiting until tomorrow morning
            await Task.Delay(sleepingTime);

            // See below
            dbContext.ResetSequence();
        }
    }
}

(Обратите внимание, что я не занимаюсь закрытием вашего приложения. Вероятно, вам нужно отменить задачу в этот момент и, возможно, что-то в этом роде)

После того, как ваша последовательность была создана, вам просто нужно запросить эту последовательность, чтобы получить новый идентификатор файла. SQL Engine автоматически обрабатывает одновременные вызовы и обеспечивает уникальность каждого возвращаемого идентификатора.

Похоже, что мы не можем выполнять необработанные запросы с помощью EF Core, как раньше делали это с EF6 (dbContext.Data.SqlQuery). Одним из решений было бы вручную выполнить команду sql. Я не знаю, как эти операции (получить соединение, открыть его и т. д.) являются потокобезопасными, поэтому я предпочитаю быть в безопасности и использовать механизм блокировки:

static class DbContextExtensions
{
    private static object DbContextLock = new object();

    public static void ResetSquence(this DbContext dbContext)
    {
        lock (DbContextLock)
        {
            using (var command = dbContext.Database.GetDbConnection().CreateCommand())
            {
                command.CommandText = @"Set @YY = (RIGHT(CONVERT(VARCHAR(8), GETDATE(), 1),2))
Set @MM =  SUBSTRING(CONVERT(nvarchar(6),getdate(), 112),5,2)
Set @DD =  RIGHT('00' + CONVERT(NVARCHAR(2), DATEPART(DAY, GETDATE())), 2)

DROP SEQUENCE IF EXISTS dbo.DailyFileId;
CREATE SEQUENCE dbo.DailyFileId
START WITH CAST(CONCAT(@YY, @MM, @DD, '0001') AS int)
INCREMENT BY 1;  
GO  ";
                command.CommandType = CommandType.Text;

                dbContext.Database.OpenConnection();

                command.ExecuteNonQuery();

                dbContext.Database.CloseConnection();
            }
        }
    }

    public static long GetNextFileId(this DbContext dbContext)
    {
        long fileId;

        lock (DbContextLock)
        {
            using (var command = dbContext.Database.GetDbConnection().CreateCommand())
            {
                command.CommandText = "SELECT NEXT VALUE FOR dbo.DailyFileId;";
                command.CommandType = CommandType.Text;

                dbContext.Database.OpenConnection();

                fileId = (long)command.ExecuteScalar();

                dbContext.Database.CloseConnection();
            }
        }

        return fileId;
    }
}

(То же самое, я не могу протестировать это, поэтому не стесняйтесь делиться исправлениями/улучшениями в комментариях, если это необходимо)

Этот метод является методом расширения, поэтому вам просто нужно назвать его так:

var newFileId = dbContext.GetNextFileId();

Для этого вам потребуется установить: Microsoft.EntityFrameworkCore.Relational .

person fharreau    schedule 03.08.2017
comment
Могу ли я сбрасывать или сбрасывать номер на каждый новый день, используя один и тот же SP вместо добавления задания в агенте SQL? - person JSON; 03.08.2017
comment
Я думаю, вы можете. Я догадывался, что ты хотел избавиться от SP. Вы должны знать, что Entity Framework позволяет отображать SP и вызывать их непосредственно из кода C#. Наверное, это был бы лучший вариант? - person fharreau; 04.08.2017
comment
действительно, я намеревался избавиться от него, не могли бы вы изучить полное решение stackoverflow.com/a/45497641/2157801 - person JSON; 04.08.2017
comment
Ваше решение должно работать, но оно включает в себя SP. Если вы действительно хотите избавиться от него, вам следует рассмотреть альтернативу, подобную моей. Если вам не нравится агент SQL, я полагаю, вы можете создать поток при запуске вашего приложения, который удаляет и воссоздает последовательность каждый день в полночь. - person fharreau; 04.08.2017
comment
Я просто добавляю код для запуска ежедневного скрипта из кода C#. - person fharreau; 04.08.2017

Пожалуйста, ваш вклад высоко ценится, поэтому я решил эту проблему с большой помощью @Pete,

Моя модель SP класса,

public class FileIdSeq

{
    [Key]
    public DateTime SequenceDate { get; set; }
    [DefaultValue(1)]
    public int LastSequence { get; set; }
}

Мой SQL-SP,

USE [ARTCORE]
GO
/****** Object:  StoredProcedure [dbo].[FileIdSeqSP]    Script Date: 08/04/2017 10:19:24 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[FileIdSeqSP]
AS
BEGIN

Declare @today date = getdate()
Declare @table table (id int)
SET NOCOUNT ON;
If Exists(select * from information_schema.columns where table_name = 'FileIdSeq' 
and column_name = 'LastSequence' 
and Table_schema = 'dbo'
and column_default is NULL)
BEGIN
ALTER TABLE [dbo].FileIdSeq ADD DEFAULT (1) FOR LastSequence
END
	
	BEGIN TRAN

UPDATE       FileIdSeq SET LastSequence = LastSequence + 1  output inserted.LastSequence into @table where SequenceDate=@today

if (@@ROWCOUNT=0)
INSERT INTO FileIdSeq (SequenceDate) 
output inserted.LastSequence into @table 
VALUES (@today)
commit


	declare @s varchar(20) 
    select @s = convert(varchar(20), id) from @table

    if (Len(@s)<4)
        set @s = Right('000' + @s, 4)
SELECT         Cast(CONVERT(VARCHAR(10), @today, 12) + @s as int) as LastSequence, SequenceDate
FROM            FileIdSeq
WHERE        (SequenceDate = @today)

END

Мой класс таблицы (модель), в котором будет создан пользовательский идентификатор файла,

 public class PatReg
{
    [NotMapped]
    private Int64 _FileId;
    [Key, Display(Name = "File Id"), ScaffoldColumn(false), DatabaseGenerated(DatabaseGeneratedOption.None)]
    public Int64 FileId
    {
        get
        {
            return this._FileId;
        }
        set
        {
            this._FileId = value;
        }
    }
    [Required, Display(Name = "First Name")]
    public string FName { get; set; } 
    [Required, Display(Name = "Middle Name")]
    public string MName { get; set; }
    [Required, Display(Name = "Last Name")]
    public string LName { get; set; }
    [Required, Display(Name = "Date of Birth"), DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    public DateTime Dob { get; set; }

}

Контроллер,

public Int64 GetSerial()
    {
        List<FileIdSeq> NewFileSeq = new List<FileIdSeq>();
        NewFileSeq = _context.FileIdSeq.FromSql("FileIdSeqSP").ToList();
        var FileID = NewFileSeq[0].LastSequence;
        return FileID;
    }
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Create([Bind("FName,MName,LName,Dob")] PatReg patReg)
    {
        if (ModelState.IsValid)
        {
            patReg.FileId = GetSerial();
            _context.Add(patReg);
            await _context.SaveChangesAsync();
            return RedirectToAction("Index");
        }
        return View(patReg);
    }

GetSerial() Генерирует последовательность, вызывая хранимую процедуру и возвращая LastSequence из строго типизированного списка

person JSON    schedule 04.08.2017