前言

前面我們已經練習過使用 Dapper,剛好前輩提起 EF 也是需要練習熟悉的,就做了一個 EF Core 的版本,基本上內容與先前 API 第二版本大致相同,這邊就只會列出差異部分與碰到的問題。

本文

一、首先我們會需要先安裝三個 EF 需要使用到的 Nuget 套件。

分別是:

Nuget名稱 安裝地點
EntityFrameworkCore Repository
EntityFrameworkCore.SqlServer Repository
EntityFrameworkCore.Tools Controller

二、接下來需要輸入指令建立 EFCore 所使用的 DBContext。

完整指令如下:

Scaffold-DbContext 'Data Source=localhost;Database=Northwind;Trusted_Connection=True;' Microsoft.EntityFrameworkCore.SqlServer -OutputDir Conditions -Tables Users -context AccountContext -Project WebApiEFCoreRepository -force

這邊提一下指令由前到後介紹:

Scaffold-DbContext 'Data Source=localhost;Database=Northwind;Trusted_Connection=True;' Microsoft.EntityFrameworkCore.SqlServer

這段是我們與 DB 連線字串,然後指定所使用的 DB 是 SQL server。

-OutputDir Conditions

這邊是生成檔案放置地點。

-Tables Users

指定的 Table 名稱。

-context AccountContext

這是會生成的 DBContext 檔案名稱,這邊我取名為 AccountContext。

-Project WebApiEFCoreRepository -force

指定生成在 Repository 專案,最後的 force 其實第一次生成 DBContext 時不用加,但後續要再加 Table 的時候可能會用上;至於我為甚麼留著呢?那當然是因為 懶惰

三、建完之後步驟。

指令建出來的 context 檔案其中有一段 DB 連線的地方這邊可以先拿掉,之後我們會使用注入的方式拿取連線字串。

接下來建立 IAccountRepository

public interface IAccountRepository
    {
        /// <summary>
        /// 取得單筆帳號資訊
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        public AccountCondition GetAccount(string account);

        /// <summary>
        /// 取得帳號列表
        /// </summary>
        /// <returns></returns>
        public IEnumerable<AccountCondition> GetAccountList();

        /// <summary>
        /// 新增帳號
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool AddAccount(AccountCondition condition);

        /// <summary>
        /// 刪除帳號
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        public bool RemoveAccount(string account);

        /// <summary>
        /// 更新帳號資訊
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool UpdateAccount(AccountCondition condition);

        /// <summary>
        /// 忘記密碼
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool ForgetPassword(AccountCondition condition);
    }

實作 AccountRepository

    public class AccountRepository : IAccountRepository
    {
        private readonly AccountContext _accountContext;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="accountContext"></param>
        public AccountRepository(AccountContext accountContext)
        {
            this._accountContext = accountContext;
        }

        /// <summary>
        /// 新增帳號
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool AddAccount(AccountCondition condition)
        {
            this._accountContext.Add(condition);
            var result = this._accountContext.SaveChanges();

            return result > 0;
        }

        /// <summary>
        /// 忘記密碼
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool ForgetPassword(AccountCondition condition)
        {
            var data = this._accountContext.Users.Find(condition.Account);
            this._accountContext.Entry(data).CurrentValues.SetValues(condition);
            var result = this._accountContext.SaveChanges();

            return result > 0;
        }

        /// <summary>
        /// 取得單筆帳號
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        public AccountCondition GetAccount(string account)
        {
            var result = this._accountContext.Users.Find(account);

            return result;
        }

        /// <summary>
        /// 取得帳號列表
        /// </summary>
        /// <returns></returns>
        public IEnumerable<AccountCondition> GetAccountList()
        {
            var result = this._accountContext.Users.ToList();

            return result;
        }

        /// <summary>
        /// 刪除帳號
        /// </summary>
        /// <param name="account"></param>
        /// <returns></returns>
        public bool RemoveAccount(string account)
        {
            var data = this._accountContext.Users.Find(account);
            this._accountContext.Remove(data);
            var result = this._accountContext.SaveChanges();

            return result > 0;
        }

        /// <summary>
        /// 更新帳號
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool UpdateAccount(AccountCondition condition)
        {
            var data = this._accountContext.Users.Find(condition.Account);
            data.Email = condition.Email;
            data.ModifyDate = condition.ModifyDate;
            data.ModifyUser = condition.ModifyUser;
            data.Phone = condition.Phone;

            var result = this._accountContext.SaveChanges();
            return result > 0;
        }
    }

接下來 Service 部分列出稍微不同的地方

        /// <summary>
        /// 更新帳號資訊
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        public ResultDto UpdateAccount(AccountInfoModel info)
        {
            if (string.IsNullOrWhiteSpace(info.Account) ||
                string.IsNullOrWhiteSpace(info.Password))
            {
                throw new Exception("請檢查輸入欄位,缺一不可!");
            }

            var accountInfo = this._accountRepository.GetAccount(info.Account);

            if (string.IsNullOrWhiteSpace(accountInfo.Password))
            {
                return new ResultDto
                {
                    Success = false,
                    Message = "請確認要更新的帳號!"
                };
            }

            var convertPassword = ConverPassword(info.Account, info.Password);

            if (accountInfo.Password != convertPassword)
            {
                return new ResultDto
                {
                    Success = false,
                    Message = "請確認輸入的密碼是否與註冊時一致!"
                };
            }

            var condition = this._mapper.Map<AccountCondition>(info);
            condition.ModifyDate = DateTime.Now;
            condition.ModifyUser = info.Account;

            var result = this._accountRepository.UpdateAccount(condition);

            return new ResultDto
            {
                Success = result,
                Message = result ? "更新成功" : "更新失敗"
            };
        }

StartUp 部分

public void ConfigureServices(IServiceCollection services)
        {
            //注入註冊
            services.AddScoped<IAccountService, AccountService>();
            services.AddScoped<IAccountRepository, AccountRepository>();
            services.AddDbContext<AccountContext>(
                options => options.UseSqlServer(@"Server=localhost;Database=Northwind;Trusted_Connection=True;"));

            //AutoMapper
            services.AddAutoMapper(
                typeof(ControllerProfile),
                typeof(ServiceProfile));

            services.AddControllers();
        }

後續步驟有問題的請參考前幾篇。 同樣的我也會把程式碼同步放上GitHub歡迎取用參考

四、重點筆記。

1、部分更新問題。
這邊遇到第一個大問題是原來 EF 並不希望你使用部分更新,因此一開始直接使用 update 馬上就報錯給我看,查了好幾種解決方式,我現在也不確定目前使用這種做法是否合適,若有更好的方法歡迎交流。
附上查詢到的 StackOverflow 連結: Entity Framework validation with partial updates

2021/04/19更新
感謝同事指正,提供了新的部分更新做法。
原本寫法

        /// <summary>
        /// 更新帳號
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool UpdateAccount(AccountCondition condition)
        {
            var data = this._accountContext.Users.Find(condition.Account);
            data.Email = condition.Email;
            data.ModifyDate = condition.ModifyDate;
            data.ModifyUser = condition.ModifyUser;
            data.Phone = condition.Phone;

            var result = this._accountContext.SaveChanges();
            return result > 0;
        }

更新後寫法

        /// <summary>
        /// 更新帳號
        /// </summary>
        /// <param name="condition"></param>
        /// <returns></returns>
        public bool UpdateAccount(AccountCondition condition)
        {
            var data = this._accountContext.Users.FirstOrDefault(
                rows => rows.Account == condition.Account);

            data.Email = condition.Email;
            data.ModifyDate = condition.ModifyDate;
            data.ModifyUser = condition.ModifyUser;
            data.Phone = condition.Phone;
            this._accountContext.Update(data)
                .Property(rows => rows.Idx).IsModified = false;

            var result = this._accountContext.SaveChanges();
            return result > 0;
        }

測試:

2、Repository Pattern Unit of Work。
這邊也看到 EF 使用 Repository 時會在隔離一個 Unit of Work 出來,研究了大約一周,還沒多大感受(其實是我偏愛 Dapper),所以這邊就使用最簡單作法。
如有興趣研究的也附上連結:

後記

這次自己從頭到尾做一次 EF 版本的 API 有一種自斷手腳的感覺,平常用 Dapper 簡單 CRUD 不用三兩下就完成的,換了一個工具還真的體會轉換陣痛期。

但 EF 是 .NET 自家指定用法身為微軟全家餐的受害者(?,免不了還是要學習一下,在此提供弱弱筆記供大家參考。