ساخت یک CRUD با ASP.NET Core و EF Core

در این مقاله درباره ی ساخت یک CRUD با ASP.NET Core و EF Core را بررسی خواهیم کرد بنابراین موضوعات پیکربندی DbContext، ذخیره سازی، کوئری زدن، تعاملات، Logging و همگامی را مورد بحث قرار خواهیم داد و در هر قسمت کد های مربوطه را در اختیار شما قرار می دهیم.

ساخت یک CRUD با ASP.NET Core و EF Core

مساله

چگونگی پیاده سازی CRUD با استفاده از Entity Framework Core و ASP.NET Core

راه حل

یک class library بسازید و پکیج های NuGet زیر را به آن اضافه کنید.

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.Relational

Microsoft.EntityFrameworkCore.SqlServer

یک کلاس بسازید که به موجودیت های پایگاه داده پاسخ می دهد.

public class Actor  
 {  
     public int Id { get; set; }  
     public string Name { get; set; }  
 }

یک کلاس بسازید که از DbContext ارث بری می کند.

public class Database : DbContext  
{  
    public Database(  
        DbContextOptions<Database> options) : base(options) { }  
  
    public DbSet<Actor> Actors { get; set; }  
}  

یک وب اپلیکیشن ASP.NET Core و یک API controller برای عملیات های CRUD بسازید تا یک لیست و یک آیتم را بازیابی کند.

[HttpGet]  
 public async Task<IActionResult> GetList()  
 {  
     var entities = await context.Actors.ToListAsync();  
     var outputModel = entities.Select(entity => new  
     {  
         entity.Id,   
         entity.Name,  
     });  
     return Ok(outputModel);  
 }  
  
 [HttpGet("{id}", Name = "GetActor")]  
 public IActionResult GetItem(int id)  
 {  
     var entity = context.Actors  
                         .Where(e => e.Id == id)  
                         .FirstOrDefault();  
     if (entity == null)  
         return NotFound();  
  
     var outputModel = new  
     {  
         entity.Id,  
         entity.Name,  
    };  
     return Ok(outputModel);  
 }  

برای ثبت یک آیتم جدید:

[HttpPost]  
  public async Task<IActionResult> Create(  
       [FromBody]ActorCreateInputModel inputModel)  
  {  
      if (inputModel == null)  
          return BadRequest();  
  
      var entity = new Actor  
      {  
          Name = inputModel.Name  
      };  
  
      this.context.Actors.Add(entity);  
      await this.context.SaveChangesAsync();  
  
      var outputModel = new  
      {  
          entity.Id,  
          entity.Name  
      };  
  
      return CreatedAtRoute("GetActor",   
                new { id = outputModel.Id }, outputModel);  
  }  

برای بروزرسانی یک آیتم موجود:

[HttpPut("{id}")]  
   public IActionResult Update(int id,   
       [FromBody]ActorUpdateInputModel inputModel)  
   {  
       if (inputModel == null || id != inputModel.Id)  
           return BadRequest();  
  
       var entity = new Actor  
       {  
           Id = inputModel.Id,  
           Name = inputModel.Name  
       };  
  
       this.context.Actors.Update(entity);  
       this.context.SaveChanges();  
  
       return NoContent();  
   }  

برای حذف یک آیتم:

[HttpDelete("{id}")]  
  public IActionResult Delete(int id)  
  {  
      var entity = context.Actors  
                          .Where(e => e.Id == id)  
                          .FirstOrDefault();  
  
      if (entity == null)  
          return NotFound();  
  
      this.context.Actors.Remove(entity);  
      this.context.SaveChanges();  
  
      return NoContent();  
  } 

پیکربندی DbContext  در Startup :

public void ConfigureServices(  
        IServiceCollection services)  
    {  
        var connection = "Data Source=...";  
  
        services.AddDbContext<Database>(options =>  
                    options.UseSqlServer(connection));  
  
        services.AddMvc();  
    }  

بحث

Entity Framework Core (EF) یک ORM است که کار با پایگاه داده را با استفاده از کلاس های POCO که موجودیت های پایگاه داده و DbContext را برای تعامل با آن ها نگاشت می کند، آسان می کند.

پیکربندی DbContext

همانطور که در کد نشان داده شده است (بخش Solution) بهترین (و قابل آزمایش) راه برای پیکربندی DbContextsubclass ،تزریق DbContextOptions در سازنده ی خودش است. NuGet پکیج Microsoft.EntityFrameworkCore یک متد اضافه ی AddDbContext برای تنظیمات سفارشی DbContext ارائه می دهد (همانطور که در قسمت Solution نشان داده شده است) شما همچنین می توانید وابستگی ها را مانند کد زیر اضافه کنید.

services.AddScoped(factory =>  
  {  
      var builder = new DbContextOptionsBuilder<Database>();  
      builder.UseSqlServer(connection);  
  
      return new Database(builder.Options);  
  }); 

ذخیره سازی

زیرکلاس DbContext موجودیت های DbSet را خواهد داشت که از طریق آن ها می توانید رکورد های پایگاه داده را به روزرسانی، حذف و یا اضافه کنید. متد SaveChanges() در DbContext تغییرات را در پایگاه داده ایجاد می کند که این به معنی آن است که می توانید آیتم های چندگانه را در یک تراکنش اضافه/ به روزرسانی/ حذف کنید.

this.context.Actors.Add(entity1);  
   this.context.Actors.Add(entity2);  
   this.context.Actors.Add(entity3);  
   await this.context.SaveChangesAsync(); 

توجه

نسخه های همگام و ناهمگام از متد SaveChanges() وجود دارد.

کوئری زدن

با استفاده از LINQ می توانید روی DbSet کوئری بزنید که یک ویژگی قوی EF است. در اینجا یک کوئری پیچیده تر برای بازیابی فیلم ها به همراه کارگردان (director) آن و بازیگران (actors) آن وجود دارد:

var entities = from movie in this.context.Movies  
                  join director in this.context.Directors  
                       on movie.DirectorId equals director.Id  
                  select new  
                  {  
                      movie.Id,  
                      movie.Title,  
                      movie.ReleaseYear,  
                      movie.Summary,  
                      Director = director.Name,  
                      Actors = (  
                        from actor in this.context.Actors  
                        join movieActor in this.context.MovieActors  
                          on actor.Id equals movieActor.ActorId  
                        where movieActor.MovieId == movie.Id  
                        select actor.Name + " as " + movieActor.Role)  
                  };  

توجه

این کوئری می تواند به طرق گوناگونی نظیر مسیریابی ویژگی ها نوشته شود اما همانطور که اشاره کردیم ما از LINQ ساده استفاده می کنیم.

توجه

نسخه های همگام و ناهمگام متد ToList() وجود دارد.

تعاملات (Transactions)

متد SaveChanges() در DbContext تسهیلات تعاملی ارائه می دهد اما شما همچنین می توانید بطور صریح یک تعامل با استفاده از DatabaseFacade در DbContext بسازید. برای نمونه در ادامه، ابتدا فیلم ها را ذخیره می کنم (تا Id آن ها را بگیرم) و سپس بازیگران را ذخیره می کنم:

using (var transaction = this.context.Database.BeginTransaction())  
  {  
      try  
      {  
          // build movie entity  
          this.context.Movies.Add(movieEntity);  
          this.context.SaveChanges();  
  
          foreach (var actor in inputModel.Actors)  
          {  
              // build actor entity  
              this.context.MovieActors.Add(actorEntity)  
          }  
          this.context.SaveChanges();  
  
          transaction.Commit();  
  
          // ...     
      }  
      catch (System.Exception ex)  
      {  
          transaction.Rollback();  
          // ...  
      }  
  }  

Logging

شما می توانید از ویژگی های logging در ASP.NET Core برای مشاهده/ ورود به SQL برای ارسال به SQL Server استفاده کنید. برای انجام این کار نیاز دارید که از interface های ILoggerProvider و ILogger استفاده کنید:

public class EfLoggerProvider : ILoggerProvider  
 {  
     public ILogger CreateLogger(string categoryName)  
     {  
         if (categoryName == typeof(IRelationalCommandBuilderFactory).FullName)  
         {  
             return new EfLogger();  
         }  
  
         return new NullLogger();  
     }  
  
     public void Dispose() { }  
 
     #region " EF Logger "  
  
     private class EfLogger : ILogger  
     {  
         public IDisposable BeginScope<TState>(TState state) => null;  
  
         public bool IsEnabled(LogLevel logLevel) => true;  
  
         public void Log<TState>(  
             LogLevel logLevel, EventId eventId, TState state,  
             Exception exception, Func<TState, Exception, string> formatter)  
         {  
             Console.WriteLine(formatter(state, exception));  
         }  
     }  
 
     #endregion  
 
     #region " Null Logger "  
  
     private class NullLogger : ILogger  
     {  
         public IDisposable BeginScope<TState>(TState state) => null;  
  
         public bool IsEnabled(LogLevel logLevel) => false;  
  
         public void Log<TState>(  
             LogLevel logLevel, EventId eventId, TState state,   
             Exception exception, Func<TState, Exception, string> formatter)  
         { }  
     }  
 
     #endregion  
 }  

شما همچنین می توانید این logger را به factory اضافه کنید:

public Startup(  
     ILoggerFactory loggerFactory)  
 {  
     loggerFactory.AddProvider(new EfLoggerProvider());  
 }  

با اجرا از طریق خط دستور، SQL شما ساخته می شود.

توجه

از متد EnableSensitiveDataLogging() در DbContextOptionsBuilder برای فعال سازی logging برای پارامترها ،که به صورت پیش فرض نمایش داده نمی شود، استفاده کنید.

همگامی (Concurrency)

همانطور که اینجا درباره ی آن بحث کردیم ما می توانیم از ETag برای پیاده سازی همگام سازی خوشبینانه استفاده کنیم اما گاهی به کنترل های بیشتری روی روند ها نیاز داریم و EF یک جایگزین دیگر ارائه می دهد. ابتدا یک فیلد در پایگاه داده (و موجودیت POCO) برای عمل به عنوان همگام سازی گرفته شده و تفسیر با ویژگی [Timestamp] می سازیم:

public class Actor  
 {  
     public int Id { get; set; }  
     public string Name { get; set; }  
     [Timestamp]  
     public byte[] Timestamp { get; set; }  
 } 

اگر شما استفاده از Fluent API را برای ساخت تنظیمات مدل به جای ویژگی [Timestamp] ترجیح می دهید می توانید از کد زیر استفاده کنید:

protected override void OnModelCreating(ModelBuilder modelBuilder)  
   {  
       modelBuilder.Entity<Actor>()  
                   .Property(actor => actor.Timestamp)  
                   .ValueGeneratedOnAddOrUpdate()  
                   .IsConcurrencyToken();  
   } 

سپس شما استثنای DbUpdateConcurrencyException را برای رفع تضاد همگام سازی دریافت خواهید کرد.

try  
   {  
       this.context.Actors.Update(entity);  
       this.context.SaveChanges();  
   }  
   catch (DbUpdateConcurrencyException ex)  
   {  
       var inEntry = ex.Entries.Single();  
       var dbEntry = inEntry.GetDatabaseValues();  
  
       if (dbEntry == null)  
           return StatusCode(StatusCodes.Status500InternalServerError,   
               "Actor was deleted by another user");  
  
       var inModel = inEntry.Entity as Actor;  
       var dbModel = dbEntry.ToObject() as Actor;  
  
       var conflicts = new Dictionary<string, string>();  
  
       if (inModel.Name != dbModel.Name)  
           conflicts.Add("Actor",   
                $"Changed from '{inModel.Name}' to '{dbModel.Name}'");  
  
       return StatusCode(StatusCodes.Status412PreconditionFailed, conflicts);  
   }  

سورس کد : GitHub