在原理 (4) 中,我們使用了特徵項 (attribute) 來處理欄位對應的問題,只是這個方法對於可能時常異動欄位名稱,或是想要利用 copy/paste 以及擴大使用範圍的需求來說,可能就沒那麼恰當,因為使用特徵項最大的缺點就是:它是寫死 (hard-code) 的,若是想要修改欄位名稱的話,勢必又要重新 compile...
在原理 (4) 中,我們使用了特徵項 (attribute) 來處理欄位對應的問題,只是這個方法對於可能時常異動欄位名稱,或是想要利用 copy/paste 以及擴大使用範圍的需求來說,可能就沒那麼恰當,因為使用特徵項最大的缺點就是:它是寫死 (hard-code) 的,若是想要修改欄位名稱的話,勢必又要重新 compile,對修改的彈性來講會是一個不小的問題。所以我們要想個方法,可以讓它們能夠有彈性一點。我們有兩種作法,一種是用介面 (interface) 來實作欄位對應,另一種則是用組態檔來對應。
我們先來講用介面對應這件事。介面的好處是大家都遵循相同的規則,只要實作了介面的物件,就一定擁有該介面的規則,利用這樣的特性,我們可以定義一個處理欄位對應的介面,其中只有一個方法,就是用來對應屬性和表格欄位。
namespace ConsoleApplication2.Contracts
{
public interface IDataSourceColumnMapper
{
string GetDataSourceColumn(string PropertyName);
}
}
有了這個介面後,我們就可以在 Entity 類別中實作這個介面,本例以原理 (4) 時使用的 Employee 類別來實作:
namespace ConsoleApplication2
{
public class Employee : Contracts.IDataSourceColumnMapper
{
public string EmployeeID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Title { get; set; }
// [DataSourceColumn("HomePhone")] // this line is for phrase 4.
public string Phone { get; set; }
public string GetDataSourceColumn(string PropertyName)
{
switch (PropertyName)
{
case "EmployeeID":
return "EmployeeID";
case "FirstName":
return "FirstName";
case "LastName":
return "LastName";
case "Title":
return "Title";
case "Phone":
return "HomePhone";
default:
throw new NotSupportedException("ERROR_PROPERTY_CANT_MAP_DATA_SOURCE_COLUMN");
}
}
}
}
然後修改資料存取程式,由原本的自特徵項取值,改成使用介面:
foreach (PropertyInfo property in properties)
{
int ordinal = -1;
Type propType = property.PropertyType;
// method 1, implement a interface to map entity and schema.
Contracts.IDataSourceColumnMapper dsMapper = employee as Contracts.IDataSourceColumnMapper;
// get column index, if not exist, set -1 to ignore.
try
{
ordinal = reader.GetOrdinal(dsMapper.GetDataSourceColumn(property.Name));
}
catch (Exception)
{
ordinal = -1;
}
// set value.
if (ordinal >= 0)
{
TypeConverters.ITypeConverter typeConverter = TypeConverters.TypeConverterFactory.GetConvertType(propType);
if (!propType.IsEnum)
{
property.SetValue(employee,
Convert.ChangeType(typeConverter.Convert(reader.GetValue(ordinal)), propType), null);
}
else
{
TypeConverters.EnumConverter converter = typeConverter as TypeConverters.EnumConverter;
property.SetValue(employee,
Convert.ChangeType(converter.Convert(propType, reader.GetValue(ordinal)), propType), null);
}
}
}
執行程式,可以得到 Employees 的資料,如同原理 (4)。介面的定義和使用並不難,只是它還是有個問題:寫死,雖然我們可以利用將 Entity 定義和 binding/use 的程式碼分開,但是還是會面臨到修改對應時的 re-compile 問題,我們真正希望做到的,是完全不用 re-compile 就能修改,所以我們就需要使用組態檔。如果對組態檔不熟,可參考本篇文章。
為了要達成欄位的定義,以及考量擴充性,我們會需要稍微複雜一點點的組態架構:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="entitySetConfiguration" type="ConsoleApplication2.Configuration.EntitySetConfiguration, ConsoleApplication2" />
</configSections>
<entitySetConfiguration>
<entities>
<entity type="ConsoleApplication2.Employee" schema="Employees">
<maps>
<map propertyName="EmployeeID" schemaName="EmployeeID" />
<map propertyName="FirstName" schemaName="FirstName" />
<map propertyName="LastName" schemaName="LastName" />
<map propertyName="Title" schemaName="Title" />
<map propertyName="Phone" schemaName="HomePhone" />
</maps>
</entity>
</entities>
</entitySetConfiguration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration>
其中,entitySetConfiguration 就是我們定義的組態檔,它的程式內容是:
namespace ConsoleApplication2.Configuration
{
public class EntitySetConfiguration : ConfigurationSection
{
[ConfigurationProperty("entities", IsRequired = false, IsKey = false)]
[ConfigurationCollection(typeof(EntityConfigurationCollection),
CollectionType = ConfigurationElementCollectionType.BasicMap, AddItemName = "entity")]
public EntityConfigurationCollection EntityConfigurations {
get { return base["entities"] as EntityConfigurationCollection; }
}
}
}
其下還有 EntityConfiguraiton, EntitySchemaMapCollection 和 EntitySchemaMap 等類別,在這裡就不貼原始碼了,請逕自下載原始碼瀏覽。
整個組態系統完成後,再來修改原始資料存取的程式:
namespace ConsoleApplication2
{
class ProgramStep4_2
{
static void Main(string[] args)
{
// step 4-2. data mapping with interface implementation.
SqlConnection db = new SqlConnection("initial catalog=Northwind; integrated security=SSPI");
SqlCommand dbcmd = new SqlCommand(@"SELECT EmployeeID, LastName, FirstName, Title, HomePhone FROM Employees", db);
List<Employee> employees = new List<Employee>();
// for method 2, prepare configuration.
Configuration.EntitySetConfiguration entitySetConfiguration =
ConfigurationManager.GetSection("entitySetConfiguration") as Configuration.EntitySetConfiguration;
Configuration.EntityConfiguration employeeMapConfiguration =
entitySetConfiguration.EntityConfigurations.GetConfigurationFromType(typeof(Employee).FullName);
db.Open();
SqlDataReader reader = dbcmd.ExecuteReader(CommandBehavior.CloseConnection);
while (reader.Read())
{
Employee employee = new Employee();
PropertyInfo[] properties = employee.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
int ordinal = -1;
Type propType = property.PropertyType;
// method 2, provision map information from configuration.
Configuration.EntitySchemaMap schemaMap =
employeeMapConfiguration.EntitySchemaMaps.GetConfigurationFromPropertyName(property.Name);
// get column index, if not exist, set -1 to ignore.
try
{
ordinal = reader.GetOrdinal(schemaMap.EntitySchemaName);
}
catch (Exception)
{
ordinal = -1;
}
// set value.
if (ordinal >= 0)
{
TypeConverters.ITypeConverter typeConverter = TypeConverters.TypeConverterFactory.GetConvertType(propType);
if (!propType.IsEnum)
{
property.SetValue(employee,
Convert.ChangeType(typeConverter.Convert(reader.GetValue(ordinal)), propType), null);
}
else
{
TypeConverters.EnumConverter converter = typeConverter as TypeConverters.EnumConverter;
property.SetValue(employee,
Convert.ChangeType(converter.Convert(propType, reader.GetValue(ordinal)), propType), null);
}
}
}
employees.Add(employee);
}
reader.Close();
db.Close();
foreach (Employee employee in employees)
{
Console.WriteLine("id: {0}, name: {1}, title: {2}, phone: {3}",
employee.EmployeeID, employee.FirstName + ' ' + employee.LastName, employee.Title, employee.Phone);
}
Console.WriteLine("");
Console.WriteLine("Press ENTER to exit.");
Console.ReadLine();
}
}
}
修改完後,試執行此程式,應可得到與前面相同的結果,不過這次我們可以試著改一下 app.config 中的欄位對應,例如把 Phone 的定義改掉,或是設成其他的欄位,再執行時就會發現資料不同 (無法對應的話會變成空白)。
Source Code: https://dotblogsfile.blob.core.windows.net/user/regionbbs/1111/201111171442484.rar