---
tags: C#進階VIP班
---
# 玩轉C#之【 第一次作業點評 通用類庫封裝實戰】
## 基本的結構建置
1 建立一個數據庫,然後執行下面的數據庫腳本,會增加兩張表 User Company,大家可以去表裡面自己多加入一些數據
2 建立數據庫表映射的基類BaseModel,包括 Id屬性
建立兩個子類Model:公司和用戶,按照表結構來
![分層架構](https://i.imgur.com/PhPKmus.png)
* 共用模組層:屬於最底層,可以被DAL、IDAL、Model、Application來引用,提供一些幫助類別
* 數據庫訪問層:主要跟數據庫溝通的,IDAL(抽象),DAL(實體)
* Model層:主要是對應數據庫一張一張的表
* 展示層:主要放控制台的程式
在Model層:
**建置 BaseModel**
```csharp=
public class BaseModel
{
public int Id { get; set; }
}
```
**建置 Company並且繼承BaseModel**
```csharp=
public class Company: BaseModel
{
public string Name { get; set; }
public DateTime CreateTime { get; set; }
public int CreatorId { get; set; }
public int LastModifierId { get; set; }
public DateTime LastModifyTime { get; set; }
}
```
**建置 User並且繼承BaseModel**
```csharp=
public class User : BaseModel
{
public string Name { get; set; }
public string Account { get; set; }
public string Password { get; set; }
public string Email { get; set; }
public string Mobile { get; set; }
public int CompanyId { get; set; }
public string CompanyName { get; set; }
public int States { get; set; }
public int UserType { get; set; }
public DateTime LastLoginTime { get; set; }
public DateTime CreateTime { get; set; }
public int CreatorId { get; set; }
public int LastModifierId { get; set; }
public DateTime LastModifyTime { get; set; }
}
```
## 基礎查詢封裝
3 提供兩個泛型的數據庫訪問方法,用 BaseModel約束
一個是用id去查詢單個實體,(只有這一個參數)
一個是查詢出數據表的全部數據列表查詢(沒有參數)
在DALL層:
**建置 BaseDAL並約束BaseModel**
:::info
* 在這邊約束是為了能正確的調用,才能確保主鍵是Id
* FROM這邊加入"[ ]"可以防止因為type.Name是關鍵字而報錯誤
* 在程式中靜態的字串,建議放到 共用模組層
:::
```csharp=
public class BaseDAL
{
public T Find<T>(int id) where T:BaseModel
{
Type type = typeof(T);
string columString =string.Join(",",type.GetProperties().Select(p=>$"[{p.Name}]"));
string sql =$"SELECT{columString} FROM[{type.Name}] WHERE Id ={id}";
T t =(T)Activator.CreateInstance(type);
using(SqlConnection conn = new SqlConnection(StaticConstant.SqlServerConnString))
{
SqlCommand command = new SqlCommand(sql,conn);
conn.Open();
SqlDataReader reader = command.ExecuteReader();
if(reader.Read())//表示有資料 開始讀
{
foreach (var prop in type.GetProperties())
{
prop.SetValue(t, reader[prop.Name]);
}
}
}
return t;
}
}
```
在Framework層:
```csharp=
piblic class StaticConstant
{
/// <summary>
/// 數據庫連線字串
/// </summary>
public static string SqlServerConnString = ConfigurationManager.ConnectionStrings["Customers"].ConnectionString;
}
```
再main中可以測試
```csharp=
static void Main(string[] args)
{
try
{
BaseDAL baseDAL = new BaseDAL();
var company = baseDAL.Find<Company>(2);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
```
疑!!!運行的時候怎麼跳出`{"類型 'System.DBNull' 的物件無法轉換成類型 'System.Int32'。"}`
![dbNULL](https://i.imgur.com/DOxpWcP.png)
**因為資料表讀出來的內容為null但model格式是設定為int**
![資料表內容null](https://i.imgur.com/unjERme.png)
![Model](https://i.imgur.com/PPZrr57.png)
因此model可以做以下修改: ?=>設定為可空類型
```csharp=
public class Company: BaseModel
{
public string Name { get; set; }
public DateTime CreateTime { get; set; }
public int CreatorId { get; set; }
public int? LastModifierId { get; set; }
public DateTime? LastModifyTime { get; set; }
}
```
並且要修改一下讀取資料庫時候的判斷
```csharp=
foreach (var prop in type.GetProperties())
{
prop.SetValue(t, reader[prop.Name] is DBNull ? null : reader[prop.Name]);
}
```
再來實作一下,**一個是查詢出數據表的全部數據列表查詢(沒有參數)**
```csharp=
public T FindAll<T>() where T:BaseModel
{
Type type = typeof(T);
string columnString =string.Join(",",type.GetProperties().Select(p=>$"[{p.Name}]"));
string sql =$"SELECT{columnString} FROM[{type.Name}]";
using(SqlConnection conn = new SqlConnection(StaticConstant.SqlServerConnString))
{
SqlCommand command = new SqlCommand(sql,conn);
conn.Open();
SqlDataReader reader = command.ExecuteReader();
Lit<T> list =new List<T>();
While(reader.Read())//表示有資料 開始讀
{
T t =(T)Activator.CreateInstance(type);
foreach (var prop in type.GetProperties())
{
prop.SetValue(t, reader[prop.Name]);
}
list.add(t);
}
}
return list;
}
```
**準備來優化一下程式碼**
增加一個私有方法:
```csharp=
private List<T> ReaderToList<T>(SqlDataReader reader)where T : BaseModel
{
Type type = typeof(T);
List<T> list = new List<T>();
while (reader.Read())//表示有資料 開始讀
{
T t = (T)Activator.CreateInstance(type);
foreach (var prop in type.GetProperties())
{
object oValue = reader[prop.name()];
if (oValue is DBNull)
oValue = null;
prop.SetValue(t, oValue);//除了guid和列舉
}
list.Add(t);
}
return list;
}
```
可以改造一下Find跟FindAll方法
![](https://i.imgur.com/rWwkqDJ.png)
## mapping映射處理
**特性可以在不破壞物件的情況下增加一點訊息**
**我們在Framework裡面中增加特性**
```csharp=
[AttributeUsage(AttributeTargets.Property)]
public class ColumAttribute:Attribute
{
public ColumnAttribute(string name)
{
this._Name = name;
}
private string _Name = null;
public string GetColumnName()
{
return _Name;
}
}
```
增加一個AttributeHelper 用來取得Attribute的方法
```csharp=
public class AttributeHelper
{
public static string GetColumnName(this PropertyInfo prop)
{
if(prop.IsDefined(typeof(ColumnAttribute),true))
{
ColumnAttribute attribute = (ColumnAttribute) prop.GetCustomAttribute(typeof(ColumnAttribute),true);
return attribute.GetColumnName();
}
return prop.Name;
}
}
```
再來改造一下 **Find跟FindAll方法以及ReaderToList**
將`{p.Name}` 替換成 `{p.GetColumnName()}`
## 新增、刪除、修改
5.進階需求:提供泛型的數據庫實體插入、實體更新、ID刪除數據的數據庫訪問方法
* 在BaseDal中加入Update,在這邊組SQL語法,**必須參數化**
* columnString 必須參數化,不能直接塞值,不然會有 sql injection
* ` p.GetValue(t) ?? DBNull.Value` =>判斷目前的變數是否為NULL 如果是就放入 DBNull.Value 如果直接放入NULL進去是會報錯誤
columnString 希望產生得格式:
```sql=
[Name]=@Name,[CreateTime]=@CreateTime,[CreatorId]=@CreatorId,[LastModifierId]=@LastModifierId,[LastModifyTime]=@LastModifyTime
```
```csharp=
public void Update<T>(T t)where T:BaseModel
{
Type type =typeof(T);
var propArray = type.GetProperties().Where(p => !p.Name.Equals("Id"));
string columnString = string.Join(",", propArray.Select(p => $"[{p.GetColumnName()}]=@{p.GetColumnName()}"));
var parameters = propArray.Select(p => new SqlParameter($"@{p.GetColumnName()}", p.GetValue(t) ?? DBNull.Value)).ToArray();
string sql =$"UPDATE{type.Name} SET{columnString} WHERE Id={t.id}";
using (SqlConnection conn = new SqlConnection(StaticConstant.SqlServerConnString))
{
using (SqlCommand command = new SqlCommand(sql, conn))
{
command.Parameters.AddRange(parameters);
conn.Open();
int iResult = command.ExecuteNonQuery();
if (iResult == 0)
throw new Exception("Update數據不存在");
}
}
}
```
6 進階需求(可選):將數據訪問層抽象,使用簡單工廠+配置文件+反射的方式,來提供對數據訪問層的使用
在此先新增一個類別庫叫Factory
新增類別叫做DALFactory
```csharp=
public class DALFactory
{
static DALFactory()
{
Assembly assembly = Assembly.Load(StaticConstant.DALDllName);
DALType = assembly.GetType(StaticConstant.DALTypeName);
}
private static Type DALType = null;
public static BaseDAL CreateInstance()
{
return (IBaseDAL)Activator.CreateInstance(DALType);
}
}
```
在StaticConstant類別中 增加
```csharp=
private static string DALTypeDll = ConfigurationManager.AppSettings["DALTypeDll"];
public static string DALDllName = DALTypeDll.Split(',')[1];
public static string DALTypeName = DALTypeDll.Split(',')[0];
```
appSetting增加
```xml=
<appSettings>
<add key="DALTypeDll" value="Homework001.DAL.BaseDAL,Homework001.DAL"/>
</appSettings>
```
## 特性通用驗證
7 進階需求(可選):按照課堂的例子,再增加幾個驗證特性如Required(非空) Mobile(手機號格式) Email(格式) 字符串長度(最大最小)等,
注意這時候一個屬性可能會有多個驗證屬性,比如Required+Email+長度
封裝成一個泛型的數據校驗方法,數據庫增刪改的時候,嘗試完成數據校驗
在Framework中加入LengAttribute、EmailAttribute、MobileAttribute..等等
* 建議每個驗證類別都創建新的類別
LengAttribute:
```csharp=
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class LengAttribute : AbstractValidateAttribute
{
private int _Min = 0;
private int _Max = 0;
public LengAttribute(int min, int max)
{
this._Min = min;
this._Max = max;
}
public override bool Validate(object value)
{
if (value != null && !string.IsNullOrWhiteSpace(value.ToString()))
{
int length = value.ToString().Length;
if (length > this._Min && length < this._Max)
{
return true;
}
}
return false;
}
}
```
EmailAttribute:
```csharp=
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class EmailAttribute : AbstractValidateAttribute
{
public override bool Validate(object oValue)
{
return oValue != null
&& Regex.IsMatch(oValue.ToString(), @"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$");
}
}
```
MobileAttribute:
```csharp=
public class MobileAttribute : AbstractValidateAttribute
{
public override bool Validate(object oValue)
{
if (oValue == null)
{
return false;
}
else
{
return Regex.IsMatch(oValue.ToString(), @"^[1]+[3,5]+\d{9}");
}
}
}
```
RegexAttribute:
```csharp=
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class RegexAttribute : AbstractValidateAttribute
{
private string _RegexExpression = string.Empty;
public RegexAttribute(string regex)
{
this._RegexExpression = regex;
}
public override bool Validate(object oValue)
{
if (oValue == null)
{
return false;
}
else
{
return Regex.IsMatch(oValue.ToString(), _RegexExpression);
}
}
}
```
在AttributeHelper擴展方法中 在加入驗證的方法
```csharp=
public static bool Validate<T>(this T tModel) where T : BaseModel
{
Type type = tModel.GetType();
foreach (var prop in type.GetProperties())
{
if (prop.IsDefined(typeof(AbstractValidateAttribute), true))
{
object[] attributeArray = prop.GetCustomAttributes(typeof(AbstractValidateAttribute), true);
foreach (AbstractValidateAttribute attribute in attributeArray)
{
if (!attribute.Validate(prop.GetValue(tModel)))
{
return false;//表示终止
//throw new Exception($"{prop.Name}的值{prop.GetValue(tModel)}不對");
}
}
}
}
return true;
}
```
當在這邊寫Validate的T作約束的時候,會發生**循環引用**的問題
,所以我們將BaseModel移至Framework中
![循環引用](https://i.imgur.com/tiGnxQY.png)
## 泛型緩存
在DAL中加入一個類別
TSqlHelper
```csharp=
static TSqlHelper()
{
Type type = typeof(T);
string columnString = string.Join(",", type.GetProperties().Select(p => $"[{p.GetColumnName()}]"));
FindSql = $"SELECT {columnString} FROM [{type.Name}] WHERE Id=";
FindAllSql = $"SELECT {columnString} FROM [{type.Name}];";
}
public static string FindSql = null;
public static string FindAllSql = null;
```
Find 修改成
```csharp=
public T Find<T>(int id) where T : BaseModel
{
Type type = typeof(T);
string sql = $"{TSqlHelper<T>.FindSql}{id};";
T t = null;
using (SqlConnection conn = new SqlConnection(StaticConstant.SqlServerConnString))
{
SqlCommand command = new SqlCommand(sql, conn);
conn.Open();
SqlDataReader reader = command.ExecuteReader();
List<T> list = this.ReaderToList<T>(reader);
t = list.FirstOrDefault();
}
return t;
}
```