65.9K
CodeProject 正在变化。 阅读更多。
Home

ADO.Net 通用数据访问层 (DAL) 重访

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (9投票s)

2020 年 4 月 5 日

CPOL

29分钟阅读

viewsIcon

29472

downloadIcon

873

一篇使用更多液氮和培根重写的旧文章。

前言

本文是对我去年发布的一个代码/文章的重大改写,反映了对该代码的修改以及基于同事们使用它的方式而产生的新想法。代码已进行了大幅更改,因此我认为它值得一篇全新的文章,而不是仅仅指向我以前的代码。此外,这段代码将用于我将来要撰写的一篇文章中的应用程序,我只想提供对本文的链接,而不是在那里重复描述所有内容(或使本已冗长的文章更长。那些注意力像两片吐司一样短的人会为我的体贴而欢欣鼓舞。看?我就是这么体贴。真的。

您可能会注意到,我修改了 CodeProject 的一些样式,例如标题和 `

` 块的背景颜色(主要是为了区分代码块和文本输出)。致 CP 员工/编辑(请除非绝对必要,否则不要随意修改这些样式)。

本文没有屏幕截图,因为类库或测试应用程序没有 GUI。相反,我将控制台输出复制粘贴到 `

` 块中,因为这样更易于阅读,并且不会违反 CodeProject 文章的图片宽度限制。

最后,友情提示——这篇内容确实有点长。我就是这样写的,在我看来, pretty much 所有文章都应该这样写。因此它们被称为“文章”而不是“技巧”。新手?看好学习。

引言

为什么不直接使用 ORM? 在开始之前,先解决这个问题。我不使用 ORM。我鄙视它们的想法,因为它们试图成为(并且需要成为)一种一刀切的解决方案,并且假设您永远不应该偏离它们所倡导的“唯一正确的道路”。此外,我不知道是谁认为使用存储过程是个坏主意,但 ORM 似乎遵循这种范式,并且通常对使用它们的支持很差。我更喜欢存储过程而不是代码内的查询,原因如下:a) 存储过程提供的固有安全性更高;b) 我发现在后端修改比部署新版本的应用程序更方便。请注意,我对争论这一点甚至进行理性讨论都不感兴趣,所以不要浪费您(或我的)时间在评论区发表这种言论。

此代码支持哪些数据库? 此代码支持 SQL Server(如果您感兴趣,我使用的是 2014 开发者版),并且应与从 2008R2 到现在的 SQL Server 所有版本兼容。其他数据库我没有兴趣,而且如果我为每一个“更好的想法”创建替代方案,编写此代码/文章将花费更长的时间。对于您——一个程序员——来说,弄清楚如何定制此代码应该是一件简单的事情。再说一遍,我对 Oracle、MySql、YourSql 或您可能使用的 WhateverSQL 的典型粉丝狂热言论不感兴趣,我也不会 receptive 或愿意帮助您修改代码以支持它们。你们都是程序员,所以自己解决问题,别烦我。

此代码在 .Net Core 中运行吗? 我知道那里的 .NET Core 用户也许也能使用此代码,但由于我(还)不使用,我不会提供支持它的代码,甚至不会提供如何将其转换为兼容的建议。如果您至少有一点自我认知(并且理所当然地认为您是程序员),您将能够轻松识别代码中需要修改以支持您自己混乱的漩涡的部分。事实上,如果您想将此代码转换为 .Net Core,请随意,但请确保发布一篇关于此的文章。事实上,您可以将其作为本文的替代方案发布。确保您完整描述转换过程。

不支持 .Net Core?真的吗? 出于好奇,我尝试将项目转换为 .Net Core。所需步骤将在本文末尾显示。

好了,闲话少说……

左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。

本文反映了我之前在一个支持两个网络(机密和非机密)、每个网络有五个环境(位于不同虚拟机上)、支持 20 个应用程序、访问超过 30 个数据库的环境中实现的技术。为简洁起见,我删除了所有与此代码无关的内容(网络、环境和应用程序),原因如下:a) 它是专有的;b) 它只是使代码混乱;c) 它不适用于本文的预期目标。如果有人在计算,这个方案支持 6000 种可能的连接字符串(尽管实际上,我们的每个应用程序只需要 4-5 个可用)。

最初,我将启用某些想法的责任推给了程序员,通过基类实体,以各种方式实现 `SqlParameter[]` 的创建。在此版本中,我包含了一种更自动化的方法,但没有取消程序员按旧方式执行的能力。这个新想法将在关于库如何使用反射的部分进行解释。

为了使此代码适合公开分发,我删除了用于管理我们工作中所面临的连接字符串混乱的连接字符串列表类,但保留了安全相关的代码,例如加密数据库和混淆连接字符串。

本文展示了一种相对通用的方法来使用 ADO.NET 并支持大量的连接字符串。不仅如此,所有连接字符串都是按需构建的,并且使程序员不必维护配置文件,除非他/她想这样做。

代码

以下是关于此代码的“值得了解”的事实:

  • 使用 Visual Studio 2017(您应该能够使用 2013 或更高版本,而无需进行大量的调整)
     
  • .NET 4.72(需要使用 4.0 或更高版本)
     
  • SQL Server 2014(应该能够使用 2008R2 或更高版本)
     
  • Windows 7 VM(是的,我是个野蛮人,但代码应该能在任何您可能使用的有缺陷的微软产品中运行)
     
  • 代码使用了反射来实现其通用性。我知道反射通常不被看好,因为它速度慢,但由于使用了泛型,反射的性能因素不大。
     
  • 在 TestHarness 应用程序中使用单例模式实现单例对象。我通常只是创建一个全局静态类来实例化全局需要的对象,但我决定走出那个特定的舒适区,尝试一些我以前没有真正用过的东西。
     
  • 除了随意提及之外,SQL 编码技巧和最佳实践几乎超出了本文的范围。使用此代码(甚至您喜欢的 ORM)并不能免除您熟悉 SQL Server 以及如何编写足够的存储过程和查询的责任。您的工作是程序员,所以做一个程序员

示例解决方案架构的实现方式是为了最小化代码的使用。您可以自由地发挥您的程序员本能,按照自己的方式进行修改。我保留了代码片段中的注释,以尽量减少冗长的文本叙述。您可能会注意到 intellisense 注释中有 html 风格的换行符。原因是 VS2019 支持这里的换行符,但旧版本不支持。旧版本缺乏这种支持使得我大量使用注释显得有些笨拙,因为它们会变得非常长,并且经常超出屏幕。这是微软的错。

带有 `[PWConnectionString]` 的连接字符串

如上所述,这里提供的 `PWConnectionString` 类是由于我们工作中所支持的极端操作环境而开发的。在我们的代码中,我们有一个 `ConnectionStringList` 类,它实现了专有代码来支持我们的基础设施要求,但没有必要说明我们在这里的真正原因。然而,如果您想混淆自己的连接字符串,或者简化在像我们这样的多环境情况下的 `web.config` 文件,它可能会很有用。

首先,我们有预期的连接字符串组件。这些属性支持连接 `string` 中最常用的属性,并且大部分是没什么特别的。

最值得注意的一点是,连接字符串是从其组件部分组装而成的,并且会自动适应 Windows 登录或数据库凭据的使用。此外,`ConnectionString` 属性将组装好的连接字符串作为 base-64 编码的字符串返回。在我们公司,我们这样做是为了在连接字符串实际被 `SqlConnection` 对象需要之前对其进行混淆。这样可以避免安全审查员在使用像 Fortify 这样的源代码安全扫描应用程序检查代码时发现问题。

#region Properties

/// <summary>
/// Get/set a flag indicating whether our connections encrypt <br/> 
/// traffic between the db and the app. The default value is <br/>
/// true.
/// </summary>
public bool EncryptTraffic { get; set; }

/// <summary>
/// Get/set the name of this object (use this name to retrieve it later)
/// </summary>
public string Name { get; set; }

/// <summary>
/// Get/set the server instance name
/// </summary>
protected string Server { get; set; }

/// <summary>
/// Get/set the name of the default catalog
/// </summary>
protected string Database { get; set; }

/// <summary>
/// Get/set the user id (if needed by the server)
/// </summary>
protected string UserID { get; set; }

/// <summary>
/// Get/set the password (if needed by the server)
/// </summary>
protected string Password { get; set; }

/// <summary>
/// Get a flag indicating whether the parameters for the connection string are valid
/// </summary>
private bool IsValid
{
    get
    {
        string value = string.Concat( this.Server,   ","
                                        ,this.Database, ","
                                        ,this.UserID,   ","
                                        ,this.Password);
        string[] parts = value.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        return (parts.Length >= 2);
    }
}

/// <summary>
/// Get the appropriate credentials needed for the server (if any). This property is used 
/// only when the connection string is being constructed for use.
/// </summary>
private string Credentials
{
    get
    {
        string value = "Integrated Security=";
        // If the userid OR passwrod are not specified, we assume that we use the windows 
        // login
        if (string.IsNullOrEmpty(this.Password) && string.IsNullOrEmpty(this.UserID))
        {
            value = string.Format("{0}true;", value);
        }
        // otherwise, we set the specified server database credentials
        else
        {
            value = string.Format("{0}false; user id={1}; password={2};", value, this.UserID, this.Password);
        }
        return value;
    }
}

/// <summary>
/// Get the part of the connection string that indicates that we encrypt the traffic between 
/// the database and the app. By default, this indicator should be true, and thus will NOT 
/// return an empty/null string.
/// </summary>
private string WithEncryptedTraffic
{
    get
    {
        string value = string.Empty;
        if (this.EncryptTraffic)
        {
            value="Encrypt=true; TrustServerCertificate=true;";
        }
        return value;
    }
}

/// <summary>
/// Get the connection string, (constructed and returned as a base64-encoded string).
/// </summary>
/// <remarks>Use the string extension method Base64Decode() to decode the string into readable text.</remarks>
public string ConnectionString
{
    get
    {
        string value = string.Empty;
        if (this.IsValid)
        {
            // build the connection string with the specified server, database, and credentials
            value = string.Format("data source={0}; initial catalog={1}; {2} {3}", this.Server, this.Database, this.Credentials, this.WithEncryptedTraffic);
        }
        else
        {
            throw new InvalidOperationException("One or more required connection string parameters were not specified.");
        }
        return value.Base64Encode();
    }
}

#endregion Properties

最后,我们重写了 `ToString()` 方法,它只返回 `ConnectionString` 属性的值。这只是一个“以防万一”的方法来获取连接字符串,并让 `ToString()` 重写发挥作用。(通常,如果您调用一个类没有重写方法的 `ToString()`,您会得到类的名称而不是有意义的内容。)

/// <summary>
/// Override that returns the base64-encoded connection string.
/// </summary>
/// <returns>The base64-encoded connection string</returns>
public override string ToString()
{
    return this.ConnectionString;
}

[DAL] (数据访问层) 类

终于,大家来这里的真正原因。`DAL` 对象正是使这一切以通用方式工作的关键。手动编写代码来识别用于插入/更新到数据库的属性的需求,坦白说,是各种公司实体开发 ORM 的主要原因。ORM 可以自动识别给定实体中存在的属性,但它们出错的地方在于它们假设一个给定的实体是数据库表的精确表示。如果您处理的是非常简单的事情,那还好,但当您开始将 ORM 应用于现实世界(不可否认有时很复杂)的问题时,它们只会碍事。

注意: `DAL` 中的几乎所有方法都是受保护的虚拟方法。原因是,一个给定的程序员完全有可能想要从派生类使用所有这些方法,或者覆盖其中一个或多个方法,甚至两者都做。我个人认为将私有非虚拟方法仅仅是为了干扰程序员使用某个类。

我的 `DAL` 对象大量使用反射(互联网上有很多关于反射是什么以及它如何在 .Net 中工作的讨论),因此编写手工代码的许多枯燥乏味的工作在很大程度上得到了缓解。但是,这并不能免除我们创建一定量代码的必要性。

首先,我们建立了一些必要的属性,并在构造函数中对其进行初始化。每个属性的目的描述在下面的代码片段中。

#region properties

/// <summary>
/// Get/set flag indicating whether the List<T> ExecuteXXX<T>() methods should <br/>
/// throw an exception if the DataTable retrieved by the query does not match the model <br/>
/// being created (it compares the number of data table columns with the number of assigned <br/>
/// values in the model). The default value is false.
/// </summary>
public bool   FailOnMismatch                 { get; set; }
/// <summary>
/// Get/set value indicating the SqlCommand object's timeout value (in seconds)
/// </summary>
public int    TimeoutSecs                    { get; set; }
/// <summary>
/// Get/(protected) set the connection string.
/// </summary>
public string ConnectionString               { get; protected set; }
/// <summary>
/// Get/set a flag indicating whether a return value parameter is added to the sql <br/>
/// parameter list if it's missing. This only applies to the SetData method <br />
/// (insert/update/delete functionality). In order for this to work, you MUST return <br />
/// @@ROWCOUNT from your stored proc.
/// </summary>
public bool   AddReturnParamIfMissing        { get; set; }
/// <summary>
/// Get/set the bulk insert batch size
/// </summary>
public int    BulkInsertBatchSize            { get; set; }
/// <summary>
/// Get/set the number of seconds before the bulk copy times out
/// </summary>
protected int BulkCopyTimeout                { get; set; }
/// <summary>
/// Get/set options flag for SqlBulkCopy operations
/// </summary>
protected SqlBulkCopyOptions BulkCopyOptions { get; set; }
/// <summary>
/// Get/set the external transaction that can be set for/used by SqlBlukCopy.
/// </summary>
protected SqlTransaction ExternalTransaction { get; set; }
/// <summary>
/// Get/set a flag indicating whether the database can be accessed from <br/>
/// the GetData or SetData methods. Allows debugging of BLL without <br/>
/// actually reading from or writing to the database. Sefault value <br/>
/// is true.
/// </summary>
public bool CanReadWriteDB                   { get; set; }

#endregion properties

有两种构造函数,您可以传入一个 `string` 或一个 `PWConnectionString` 对象。

#region constructors

/// <summary>
/// Create instance of the DAL, and set default values for properties.
/// </summary>
/// <param name="connStr"></param>
/// <exception cref="ArgumentNullException">The connStr parameter cannot be null/empty.</exception>
public DAL(string connStr)
{
    if (string.IsNullOrEmpty(connStr))
    {
        throw new ArgumentNullException("connStr");
    }
    // this connection string *should* be base64 encoded
    this.ConnectionString        = connStr;
    this.Init();
}

/// <summary>
/// Constructor (calls the other overload to set the connection string)
/// </summary>
/// <param name="conStr">The AefConnectionString object to use for this DAL instance.</param>
public DAL(PWConnectionString connStr) : this(connStr.ConnectionString)
{
}

#endregion constructors

DAL 有两个基本功能——获取数据和设置数据。设置数据进一步细分为一次设置一条记录,以及进行“批量”插入。所有这些处理都利用了反射,这正是使此代码适用于您可能遇到的任何实体的关键。

它如何使用反射

让我们从定义一个实体开始(我在这里使用的示例实体都在解决方案中包含的测试工具应用程序中)。这是一个简单的实体,它有六个公共属性。请注意,没有任何属性被修饰,并且所有属性都有 `set` 方法。它们也是虚拟的,以便我稍后在文章中(作为示例)进行重载。

public class MyEntity
{
    public virtual string   StringValue   { get; set; }
    public virtual int      IntValue      { get; set; }
    public virtual double   FloatValue    { get; set; }
    public virtual bool     BoolValue     { get; set; }
    public virtual DateTime DateTimeValue { get; set; }
    public virtual string   AnotherString { get; set; }
}

通常,当您使用 ADO 时,您不仅要编写访问数据库的代码,还要处理 `SqlParameter` 数组以保存数据,或者在检索数据时手动反序列化您收到的结果集。这个 DAL 对象通过使用反射消除了这两种数据库访问的处理方式。

静态 `[DAL]` 方法

首先,我承认有很多方法可以解决这个问题,我的方法只是其中一种。我可以将一个方法添加到现有的 `ObjectExtensions.TypeExtensions` 类中,但我希望将所有这些东西放在一个地方。这纯粹是艺术上的决定,所以不要认为这是“唯一正确的方式”。也许有一天,当我无聊的时候,我会重新组织代码。但也可能不会。

静态 `SqlParameters` 类包含一个静态方法,该方法允许您从指定的实体对象构建 `SqlParameter` 数组。

/// <summary>
/// Creates a SqlParameter array from the specified entity, and based on the specified <br/>
/// bulk insert type
/// </summary>
/// <typeparam name="T">The entity type</typeparam>
/// <param name="entity">The entity object</param>
/// <param name="bulkType">The bulk insert type.</param>
/// <param name="precedence">How to treat the discovered SqlParameter[] property (if any)</param>
/// <param name="propertyName">Case-sensitive name of desired SqlParameter[] property</param>
/// <returns>An appropriate SqlParameter array </returns>
/// <exception cref="ArgumentNullException">The entity cannot be null</exception>
/// <exception cref="InvalidOperationException">The propertyName should never be null.</exception>
protected static SqlParameter[] MakeSqlParameters<T>(T entity, 
                                                        BulkInsertType bulkType, 
                                                        ParamPrecedence precedence = ParamPrecedence.None, 
                                                        string propertyName = "SqlParameters")
{
    if (entity == null)
    {
        throw new ArgumentNullException("entity");
    }
    if (string.IsNullOrEmpty(propertyName))
    {
        throw new InvalidOperationException("It makes no sense to specify a null propertyName. Ever.");
    }

    SqlParameter[] parameters = null;
    PropertyInfo[] properties = entity.GetType().GetProperties();

    // see if we can find the specified propertyName that returns a SqlParameter[] (with the right name)
    PropertyInfo sqlParams = sqlParams = properties.FirstOrDefault(x => x.PropertyType.Name == "SqlParameter[]" && 
                                                                        x.Name == propertyName);

    // if the entity has a property that returns a SqlParameter array AND the calling 
    // method did not specify to ignore it, set the parameters var to that property's 
    // value, and our job is done here
    if (sqlParams != null && precedence != ParamPrecedence.UseBulkType)
    {
        parameters = (SqlParameter[])sqlParams.GetValue(entity);
    }
    else
    // looks like we gotta finger it out on our own - NOBODY EXPECTED THE MANUAL DETECTION!!
    {
        List<SqlParameter> list = new List<SqlParameter>();

        properties = DAL.GetEntityProperties(properties, bulkType);

        // populate the list of SqlPrameters from the properties we gatherd in the switch statment.
        foreach(PropertyInfo property in properties)
        {
            list.Add(new SqlParameter(string.Format("@{0}", property.Name), property.GetValue(entity)));	
        }
        parameters = list.ToArray();
    }

#if DEBUG
    Global.WriteLine("-----------------------------"); 
    if (properties.Length == 0)
    {
        Global.WriteLine("No properties found.");
    }
    else
    {
        // satisfy my fanatical desire to line stuff up.
        int length = parameters.Max(x=>x.ParameterName.Length) + 1;
        string format = string.Concat("    {0,-", length.ToString(), "}{1}");

        //// i thought this was providing redundant info, but only commented it out so I can 
        //// easily get it back if needed.
        //Global.WriteLine("Discovered properties:");
        //foreach(PropertyInfo item in properties)
        //{
        //	string text = string.Format(format, item.Name, item.GetValue(entity).ToString());
        //	Global.WriteLine(text); 
        //}
        Global.WriteLine("Resulting parameters:");
        foreach(SqlParameter item in parameters)
        {
            string text = string.Format(format, item.ParameterName, item.SqlValue);
            Global.WriteLine(text);
        }
    }
#endif

    // and return them to the calling method
    return parameters;
}

尽管已包含注释,但我仍然觉得有必要对参数进行更多解释。

  • T entity - 这是实体对象,其中包含我们将要在 `SqlParameter` 数组中返回的属性。
     
  • BulkInsertType bulkType - 指示您希望如何填充生成的 `SqlParameter` 数组。
    • ALL - 使 `MakeParameters` 方法返回所有属性,无论其状态如何。它仅包含以供完整性参考,您最有可能永远不会使用此值,尤其是在 `DAL` 中使用时。
    • DBInsertAttribute - 使 `MakeParameters` 方法返回任何带有 `[CanDbInsert]` 属性修饰的属性。这可能是您使用最多的(如果您没有在实体中使用一个或多个 `SqlParameter[]` 属性)。
    • HasSetMethod - 使 `MakeParameters` 方法返回任何具有 `set` 方法的属性。
  • ParamPrecedence precedence - 指示您是否要使用或忽略指定的批量插入类型(默认值为 `ParamPrecedence.None`)。
    • None - 使 `MakeParameters` 方法返回名为“SqlParameters”的“标准”`SqlParameter[]` 属性(如果存在)。如果不存在,则使用指定的 `BulkInsertType` 值来确定如何获取属性。
    • UseBulkType - 使 `MakeParameters` 方法*忽略*实体中的任何/所有 `SqlParameter[]` 属性,并根据指定的 `bulkType` 值获取属性。
  • string propertyName - 您希望使用的实体对象中返回 `SqlParameter[]` 的 `SqlParameter[]` 属性的名称(默认值为“SqlParameters”)。此参数仅在您指定 `precedence` 为 `None` 时才有用。

正如您所见,生成的 `SqlParameter` 项数组取决于您调用 `MakeParameters` 方法时指定的参数组合。我试图充分注释该方法,以便没有或很少有关于正在发生的事情的问题。

接下来,我们有一些由 `DAL` 中的方法使用的支持性静态方法,这些方法是 `GetEntityProperties` 方法的三个重载。为简洁起见,我将仅显示每个方法的原型。

protected static PropertyInfo[] GetEntityProperties(Type type, BulkInsertType bulkType)...

此重载根据 `bulkInsert` 参数的值获取实体的属性,并用于辅助创建 `DataTable` 对象中的列。

protected static PropertyInfo[] GetEntityProperties<t>(T entity, BulkInsertType bulkType)
</t>

此重载返回实体的属性信息(包括值)(或类),基于 `bulkInsert` 参数的值(它实际上调用下一个重载方法),并且由实际执行数据库相关工作的 `DAL` 方法使用。

protected static PropertyInfo[] GetEntityProperties(PropertyInfo[] properties, BulkInsertType bulkType)

这是基于 `bulkInsert` 参数的值执行实际工作的重载,如果您已经从另一个来源(可能与此代码无关)获得了 `SqlParameter[]`,您可以使用它。

测试(使用 TestHarness 应用程序)

注意: 我们打断我们正常的类描述,为您带来关于测试的重要(且有先见之明)的公告,因为这是我们能想到的让您有机会反思反射效果的最佳方式。因此,在不作任何进一步偏离(或不当推迟)的情况下,在这一部分讨论使用反射进行类型检测……

为了辅助测试,我在解决方案中添加了一个 TestHarness 应用程序项目(稍后描述)。在该应用程序中,我创建了一个 `BLL` 对象,并向其中添加了一些测试方法。下面是 BLL 调用继承的 `DAL` 方法时每个调用的控制台应用程序输出。我创建了一个 `MyEntity` 对象列表,每个对象有五个项。为简洁起见,我只显示一个项目的输出(因为所有五个都相同)。

关于 `MyEntity` 类的关键点——它包含六个未修饰的属性,并且没有 `SqlParameter[]` 属性。这意味着两个默认参数不会影响结果,因此未指定。

==========================================================
The method call that was made:
ExecuteStoredProc(e.AsEnumerable(), "test", BulkInsertType.ALL);

The output that was generated:
Because there are no SqlParameter[] properties, it returned all of the parameters.
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 8:54:54 AM
    @AnotherString 101B

==========================================================
The method call that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute)

The output that was generated:
Because there are no SqlParameter[] properties, and because none of the properties were decorated 
with the [CanDbInsert] attribute, it returned no parameters.
-----------------------------
No properties found.

==========================================================
The method call that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.HaveSetMethod)

The output that was generated:
Because there are no SqlParameter[] properties, and because all of the properties have a "set" 
method, it returned six parameters.
-----------------------------
(should get six props/params for each entity)
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 8:54:54 AM
    @AnotherString 101B

现在让我们添加一个 `SqlParameter[]` 属性。为了方便示例,我们将简单地继承 `MyEntity` 类。请注意,我使用了此属性的“标准”名称来稍微测试一下代码。

public class MyEntity2 : MyEntity
{
	// use the standard name that the code knows to look for
    public virtual SqlParameter[] SqlParameters
    {
        get
        {
            SqlParameter[] value = new SqlParameter[]
            {
                new SqlParameter ("@StringValue",   this.StringValue  )
                ,new SqlParameter("@IntValue",      this.IntValue     )
                ,new SqlParameter("@FloatValue",    this.FloatValue   )
                ,new SqlParameter("@BoolValue",     this.BoolValue    )
                ,new SqlParameter("@DateTimeValue", this.DateTimeValue)
            };
            return value;
        }
    }
}

因为我们添加了 `SqlParameter[]` 属性,所以我们有两倍的方式来使用有意义的方法。

==========================================================
The method call(s) that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.ALL, ParamPrecedence.None);
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute, ParamPrecedence.None);
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.HaveSetMethod, ParamPrecedence.None);

The output that was generated:
All three of those method calls will retrieve 5 of the 6 available properties 
from the [SqlParameters] property , because and precedence is [None], thus 
ignoring the [bulkInsert] value.
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 9:12:54 AM


==========================================================
The method call(s) that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.ALL, ParamPrecedence.UseBulkType)

The output that was generated:
The method call above will return all properties, regardless of any status 
they may have, because precedence is set to [UseBulkType], thus ignoring 
any SqlParameter[] properties that might be defined. Furthermore, note that 
the SqlParameter[] property is also returned, which is probably not the 
desired result (this is why using [ALL] is not recommended within the 
context of using the DAL).
-----------------------------
Resulting parameters:
    @SqlParameters
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 9:12:54 AM
    @AnotherString 101B

==========================================================
The method call that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.HaveSetMethod, ParamPrecedence.UseBulkType)

The output that was generated:
This call will return any property that has a [set] method because the 
precedence is set to [UseBulkType], thus ignoring any SqlParameter[] 
properties that might be defined. Note that since the SqlParameter[] 
property doesn't have a [set] method, it is  not returned as a 
parameter.
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 9:12:54 AM
    @AnotherString 101B

==========================================================
The method call that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute, ParamPrecedence.UseBulkType)

The output that was generated:
This method call returns no parameters, because bulkType is set to 
[DBInsertAttribute] with no properties being decorated with the [CanDbInsert] 
attribute, and because precedence is set to [UseBulkType], which causes any 
SqlParameter[] properties to be ignored.
-----------------------------
No properties found.

最后,让我们派生一个新类——`MyEntity3`——从 `MyEntity2` 派生,修饰六个属性中的五个,并添加第二个 `SqlParameter[]` 属性。

public class MyEntity3 : MyEntity2
{
    [CanDbInsert]
    public override string   StringValue   { get; set; }
    [CanDbInsert]
    public override int      IntValue      { get; set; }
    [CanDbInsert]
    public override double   FloatValue    { get; set; }
    [CanDbInsert]
    public override bool     BoolValue     { get; set; }
    [CanDbInsert]
    public override DateTime DateTimeValue { get; set; }

    public SqlParameter[] SqlParametersEntity3
    {
        get
        {
            SqlParameter[] value = new SqlParameter[]
            {
                    new SqlParameter("@StringValue",   this.StringValue)
                ,new SqlParameter("@IntValue",      this.IntValue)
            };
            return value;
        }
    }
}

再次,`MyEntity3` 类为我们提供了更多使用 `MakeParameters` 方法的方式。

==========================================================
The method call(s) that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute, ParamPrecedence.None)

The output that was generated:
This method call returns 5 of 6 parameters because precedence is None, and the 
entity contains a SqlParameters prop). The [bulkType] is ignored.
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 12:11:39 PM

==========================================================
The method call(s) that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute, ParamPrecedence.None, "SqlParametersXYZ")

The output that was generated:
This method call returns 5 of 6 parameters because  a) precedence is set to 
[None], b) the specified [propertyName] doesn't exist, but c) the class *does* 
have decorated properties.
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 12:11:39 PM

==========================================================
The method call(s) that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute, ParamPrecedence.None, "SqlParametersEntity3")

The output that was generated:
This method call gets the two properties because a) the precedence is set to 
[None], and b) the named SqlParameter property was found, thus ignoring the 
specified [bulkType].
-----------------------------
Resulting parameters:
    @StringValue 101
    @IntValue    1

==========================================================
The method call(s) that was made:
ExecuteStoredProc(e.AsEnumerable(),"test", BulkInsertType.DBInsertAttribute, ParamPrecedence.UseBulkType)

The output that was generated:
This method call returns 5 of 6 properties because the the [bulkType] is set 
to [DBInsertAttribute], the class has decorated properties, and the 
precedence is set to [UseBulkType], thus ignoring any SqlParameter[] 
properties that might be defined.
-----------------------------
Resulting parameters:
    @StringValue   101
    @IntValue      1
    @FloatValue    1
    @BoolValue     True
    @DateTimeValue 4/3/2021 12:11:39 PM

正如您所见,`MakeParameters` 方法的结果可能因您调用它的方式而异。

现在您已经了解了反射在支持 `DAL` 方面的用法,让我们来谈谈获取和设置数据。

获取和设置数据

为了检索数据,`DAL` 实现了一个重载的 `protected virtual` 方法。这些方法通常由您的 BLL 对象使用。

方法 - `[ExecuteStoredProc]`

ExecuteStoredProc 重载允许您使用存储过程来获取或设置数据。由 .Net 选择的重载取决于返回值和您使用的参数。下面块中的最后一个方法是为了允许给定的“设置数据”操作使用持久化的 `SqlConnection` 对象来处理指定集合中的所有对象,旨在提供高效的“upsert”功能。我们的一位程序员担心为每次插入数据库都打开一个 `SqlConnection` 对象。我无法想象这不会更有效率,但说实话,我没有进行任何测试来用有形的结果来证实或否定这个想法。

/// <summary>
/// Executes the named stored proc (using ExecuteReader) that gets data from the database. <br/> 
/// </summary>
/// <typeparam name="T">The type of the list item</typeparam>
/// <param name="storedProc">The name of the stored procedure to execute</param>
/// <param name="parameters">The parameters to pass to the stored procedure</param>
/// <returns>A list of the specified object type (may be empty).</returns>
/// <remarks>Useage: List<MyObject> list = this.ExecuteStoredProc<MyObject>(...)</remarks>
/// <exception cref="ArgumentNullException">The storedProc parameter cannot be null/empty.</exception>
protected virtual List<T> ExecuteStoredProc<T>(string storedProc, SqlParameter[] parameters)
{
	if (string.IsNullOrEmpty(storedProc))
	{
		throw new ArgumentNullException("storedProc");
	}
	// get the data from the database
	DataTable data = this.GetData(storedProc, parameters, CommandType.StoredProcedure);
	List<T> collection = this.MakeEntityFromDataTable<T>(data);
	return collection;
}

/// <summary>
/// Executes the specified stored proc (using ExecuteNonQuery) that stores data (specified <br/>
/// in the [parameters] parameter) in the database. 
/// </summary>
/// <param name="storedProc">The stored proc to execute</param>
/// <param name="parameters">The parameters to pass to the stored procedure</param>
/// <returns>The number of records affected</returns>
/// <exception cref="ArgumentNullException">The [storedProc] parameter cannot be null/empty.</exception>
/// <exception cref="ArgumentNullException">The [parameters] array parameter cannot be null.</exception>
/// <exception cref="ArgumentNullException">The [parameters] array parameter cannot be empty.</exception>
protected virtual int ExecuteStoredProc(string storedProc, SqlParameter[] parameters)
{
	if (string.IsNullOrEmpty(storedProc))
	{
		throw new ArgumentNullException("storedProc");
	}
	if (parameters == null)
	{
		throw new ArgumentNullException("parameters");
	}
	if (parameters.Length == 0)
	{
		throw new InvalidOperationException("The [parameters] array must contain at least one item.");
	}
	// You must SET NOCOUNT OFF at the top of your stored proc in order to automatically 
	// return the @@ROWCOUNT value (created and set by sql server).
	int result = this.SetData(storedProc, parameters, CommandType.StoredProcedure);

	return result;
}

/// <summary>
/// Execute the specified stored proc to save the specified data item in the database.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data">The data item to save</param>
/// <param name="storedProc">The stored proc to execute</param>
/// <param name="bulkType">How to build the SqlParameter array</param>
/// <param name="precedence">Whether to ignore the discovered SqlParameter[] property</param>
/// <param name="paramArrayPropName">Name of desired SqlParameter[] property</param>
/// <returns>The number of records affected</returns>
protected virtual int ExecuteStoredProc<T>(T data, 
											string storedProc, 
											BulkInsertType bulkType, 
											ParamPrecedence precedence = ParamPrecedence.None, 
											string paramArrayPropName = "SqlParameters")
{
	int result = 0;
	SqlParameter[] parameters = DAL.MakeSqlParameters(data, bulkType, precedence, paramArrayPropName);
	result = this.ExecuteStoredProc(storedProc, parameters);
	return result;
}

/// <summary>
/// Executes the specified stored proc for an entity collection, using a persistent <br/>
/// sql connection. This is intended to be used for data that needs to be merged <br/>
/// (update or insert) into a table rather than simply inserted.
/// </summary>
/// <typeparam name="T">The type of entity represented by the specified collection</typeparam>
/// <param name="data">The collection of entities</param>
/// <param name="storedProc">The name of the stored proc (must be a stored proc)</param>
/// <param name="bulkType">Inidcates how properties should be retrieved from the entity items</param>
/// <param name="precedence">Whether to ignore the discovered SqlParameter[] property</param>
/// <param name="paramArrayPropName">Name of desired SqlParameter[] property</param>
/// <exception cref="ArgumentNullException">If the data parameter is null</exception>
/// <exception cref="ArgumentNullException">If the storedProc parameter is null/empty</exception>
/// <exception cref="InvalidOperationException">If the data collection is empty.</exception>
/// <returns>The number of records affected (inserted + updated).</returns>
/// <remarks>Usage:  int result = ExecuteStoredProc(data.AsEnumerable(), "dbo.MyStoredProc", BulkInsertType.DBInsertAttribute);</remarks>
protected virtual int ExecuteStoredProc<T>(IEnumerable<T> data, 
											string storedProc, 
											BulkInsertType bulkType,
											ParamPrecedence precedence = ParamPrecedence.None, 
											string paramArrayPropName = "SqlParameters")
{
	if (string.IsNullOrEmpty(storedProc))
	{
		throw new ArgumentNullException("storedProc");
	}
	if (data == null)
	{
		throw new ArgumentNullException("data");
	}
	if (data.Count() == 0)
	{
		throw new InvalidOperationException("Data collection must contain at least onme item");
	}

	int result = this.DoBulkMerge(data, storedProc, bulkType, CommandType.StoredProcedure, precedence, paramArrayPropName);
	return result;
}

注意上面的最后两个重载。你们中的一些人可能知道,通常调用这些重载之一会导致 .Net 执行 `(T entity...)` 重载,即使您传递的是一个对象集合而不是单个对象。原因是,在使用泛型类型时,.Net 将对象集合视为与非集合对象相同,因此它会找到并使用最合适的重载。但并非毫无希望。您只需要更具体地说明您传递给方法的内容。请参阅下面的示例代码。

// If you're working with a single object, it works as expected, and 
// will execute the overload with the prototype "(T entity, ...)""
MyObject oneObject = new MyObject;
int result = this.ExecuteStoredProc(oneObject, ...);

// However, if you want to use the overload with the prototype 
// "(IEnumerable<t> entity, ...)", you simply have to call the 
// collection's AsEnumerable() method.
List<MyObject> manyObj = new List<MyOIbject>()
{ 
    new MyObject(), 
    new MyObject() 
};
int result = this.ExecuteStoredProc(oneObject.AsEnumerable(), ...);
</t>

请注意,`ExecuteQuery` 方法重载(稍后讨论)也需要这种参数操作才能让 .Net 选择所需的重载。

方法 - `[ExecuteQuery]`

与 `ExecuteStoredProc` 方法一样,`ExecuteQuery` 方法允许程序员获取或设置数据,并且为此实现了几个重载。上面关于 `ExecuteStoredProc` 的所有评论也适用于此方法,只是这个存储过程旨在执行一个直接的 SQL 查询。

/// <summary>
/// Executes the specified query (using ExecuteReader) that gets data from the database.
/// </summary>
/// <typeparam name="T">The type of the list item</typeparam>
/// <param name="query">The query text to execute</param>
/// <param name="parameters">The data to pass to the query text</param>
/// <returns>A list of the specified type.</returns>
/// <remarks>Useage: List<MyObject> list = this.ExecuteStoredProc<MyObject>(...)</remarks>
/// <exception cref="ArgumentNullException">If the query parameter is null/empty</exception>
protected virtual List<T> ExecuteQuery<T>(string query, params SqlParameter[] parameters)
{
    // If you have questions regarding the use of parameters in query text, google 
    // "c# parameterized queries". In short, parameterized queries prevent sql 
    // injection. This code does not (and cannot) validate the use of parameters 
    // because some queries simply don't need them. Therefore, it's completely 
    // on you - the developer - to make sure you're doing it right.

    if (string.IsNullOrEmpty(query))
    {
        throw new ArgumentNullException("query");
    }
    DataTable data = this.GetData(query, parameters, CommandType.Text);
    List<T> collection = this.MakeEntityFromDataTable<T>(data);
    return collection;
}

/// <summary>
/// Executes the specified query text (using ExecuteNonQuery) that stores data in the <br/>
/// database. 
/// </summary>
/// <param name="query"></param>
/// <param name="parameters"></param>
/// <returns>The number of records affected (if you didn't use SET NOCOUNT ON in 
/// your batch)</returns>
/// <exception cref="ArgumentNullException">If the query parameter is null/empty</exception>
protected virtual int ExecuteQuery(string query, params SqlParameter[] parameters)
{
    // If you have questions regarding the use of parameters in query text, google 
    // "c# parameterized queries". In short, parameterized queries prevent sql 
    // injection. This code does not (and cannot) validate the use of parameters 
    // because some queries simply don't need them. Therefore, it's completely 
    // on you - the developer - to make sure you're doing it right.
    if (string.IsNullOrEmpty(query))
    {
        throw new ArgumentNullException("query");
    }
    // Save the data to the database. If you use SET NOCOUNT ON in your query, the return 
    // value will always be -1, regardless of how many rows are actually affected.
    int result = this.SetData(query, parameters, CommandType.Text);
    return result;
}

/// <summary>
/// Execute the specified sql query to save the specified data item. This method creates the <br/>
/// parameters for you using the properties in the specified entity. While not required, you <br/>
/// can "grease the skids" by implementing a public property in your entities that returns a <br/>
/// SqlParameter[] array.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data">The data item to save</param>
/// <param name="query">The sql query text to execute</param>
/// <param name="bulkType">The flag that indicates how to build the SqlParameter array</param>
/// <returns>The number of records affected.</returns>
protected virtual int ExecuteQuery<T>(T data, 
                                        string query, 
                                        BulkInsertType bulkType,
                                        ParamPrecedence precedence = ParamPrecedence.None, 
                                        string paramArrayPropName = "SqlParameters")
{
    int result = 0;
    SqlParameter[] parameters = DAL.MakeSqlParameters(data, bulkType);
    result = this.ExecuteQuery(query, parameters);
    return result;
}

/// <summary>
/// Performs an insert of an entity collection, using a persistent connection (reduces <br/>
/// processing time and memory consumption because we're not opening/closing a database <br/>
/// connection for every item). This method is intended to be used for data that needs <br/>
/// to be merged (update or insert) into a table rather than simply inserted.
/// </summary>
/// <typeparam name="T">The type of entity represented by the specified collection</typeparam>
/// <param name="data">The collection of entities</param>
/// <param name="storedProc">The name of the stored proc (must be a stored proc)</param>
/// <param name="bulkType">Inidcates how properties should be retrieved from the entity items</param>
/// <exception cref="ArgumentNullException">If the data parameter is null</exception>
/// <exception cref="ArgumentNullException">If the query parameter is null/empty</exception>
/// <exception cref="InvalidOperationException">If the data collection is empty.</exception>
/// <returns>The number of records affected.</returns>
protected virtual int ExecuteQuery<T>(IEnumerable<T> data, 
                                        string query, 
                                        BulkInsertType bulkType,
                                        ParamPrecedence precedence = ParamPrecedence.None, 
                                        string paramArrayPropName = "SqlParameters")
{
    if (string.IsNullOrEmpty(query))
    {
        throw new ArgumentNullException("query");
    }
    if (data == null)
    {
        throw new ArgumentNullException("data");
    }
    if (data.Count() == 0)
    {
        throw new InvalidOperationException("Data collection must contain at least onme item");
    }
    int result = this.DoBulkMerge(data, query, bulkType, CommandType.Text, precedence, paramArrayPropName);
    return result;
}

方法 - `[ExecuteBulkInsert]`

将数据插入数据库的一个重要方面是能够将数据直接填入表。因此,这个 `DAL` 对象实现了执行此操作的功能。您可以选择使用 `DataTime` 对象,或使用您自己的实体集合。

/// <summary>
/// Performs a simply bulk insert into a table in the database. The schema MUST be part of <br/>
/// the table name. Using a bulk insert is NOT suitable if you need to merge data into an <br/>
/// existing table. Use the BulkMerge stored proc instead.
/// </summary>
/// <param name="dataTable">The datatable to bulk copy</param>
/// <returns>The number of records affected.</returns>
/// <exception cref="InvalidOperationException">If the table name hasn't been specified in the datatable</exception>
/// <exception cref="InvalidOperationException">If the schema hasn't been specified as part of the tablename</exception>
/// <exception cref="InvalidOperationException">If the dataTable is empty</exception>
protected virtual int ExecuteBulkInsert(DataTable dataTable)
{
	if (string.IsNullOrEmpty(dataTable.TableName))
	{
		throw new InvalidOperationException("The table name MUST be specified in the datatable (including the schema).");
	}
	if (!dataTable.TableName.Contains('.') || dataTable.TableName.StartsWith("."))
	{
		throw new InvalidOperationException("The schema MUST be specified with the table name.");
	}
	if (dataTable.Rows.Count == 0)
	{
		throw new InvalidOperationException("The dataTable must contain at least one item");
	}

	int recsBefore = BulkInsertTargetCount(dataTable.TableName);

	int           recordsAffected = 0;
	SqlConnection conn            = null;
	SqlBulkCopy   bulk            = null;

	using (conn = new SqlConnection(this.ConnectionString.Base64Decode()))
	{
		conn.Open();
		using (bulk = new SqlBulkCopy(conn, this.BulkCopyOptions, this.ExternalTransaction)
		{
			BatchSize             = this.BulkInsertBatchSize
			,BulkCopyTimeout      = this.BulkCopyTimeout
			,DestinationTableName = dataTable.TableName
		})
		{
			Debug.WriteLine("DoBulkInsert - inserting {0} rows",dataTable.Rows.Count);
			bulk.WriteToServer(dataTable);
		}
	}

	int recsAfter = BulkInsertTargetCount(dataTable.TableName);
	recordsAffected = recsAfter - recsBefore;
	return recordsAffected;
}

/// <summary>
///	Performs a simple bulk insert into a table in the database. The schema MUST be part of <br/>
/// the table name.
/// </summary>
/// <typeparam name="T">The entity type being bulk inserted</typeparam>
/// <param name="data">The list of entities to be inserted</param>
/// <param name="tableName">The table name in which to insert the data</param>
/// <param name="byDBInsertAttribute">If true, only properties decorated with the CanDbInsert 
/// attribute will be stored in the database.</param>
/// <returns>Number of records affected.</returns>
/// <exception cref="ArgumentNullException">If the data parameter is null.</exception>
/// <exception cref="ArgumentNullException">If The tableName parameter cannot be null/empty.</exception>
/// <exception cref="InvalidOperationException">If the data collection is empty.</exception>
/// <exception cref="InvalidOperationException">If the table name doesn't include the schema.</exception>
protected virtual int ExecuteBulkInsert<T>(IEnumerable<T> data, 
										string tableName, 
										BulkInsertType bulkType, 
										ParamPrecedence precedence = ParamPrecedence.None, 
										string paramArrayPropName = "SqlParameters")
{
	// sanity checks
	if (data == null)
	{
		throw new ArgumentNullException("data");
	}
	if (data.Count() == 0)
	{
		throw new InvalidOperationException("The data collection must contain at least one item");
	}
	if (string.IsNullOrEmpty(tableName))
	{
		throw new ArgumentNullException("The tableName parameter cannot be null or empty.");
	}
	if (!tableName.Contains('.'))
	{
		throw new InvalidOperationException("The schema MUST be specified with the table name.");
	}

	int result = 0;
	DataTable dataTable = null;

	if (data.Count() > 0)
	{
		dataTable = this.MakeDataTable(data, tableName, bulkType, precedence, paramArrayPropName);
		result = this.DoBulkInsert(dataTable);
	}
	return result;
}

可能的 `[SqlParameter[]]` 属性方法

如果您将静态方法 `MakeParameters` 更改为 `public`(在本文中呈现为 `protected`),您可以像这样从您的实体中调用它:

public SqlParameter[] SqlParameters4
{
    get
    {
        SqlParameter[] value = DAL.MakeSqlParameters(this, 
                                                     BulkInsertType.DBInsertAttribute, 
                                                     ParamPrecedence.UseBulkType);
        return value;
    }
}

坦白说,我认为这是浪费精力,因为 `DAL` 已经为您做了这一切(因为它也使用 `MakeParameters`,并且以相同的方式)。但是,我想说明这确实是可能的。还值得一提的是,您也可以使 `DAL.GetProperties` 方法 `public` 并在您的实体内部使用它们。为了方便您,我在 `DALStaticMethods.cs` 文件的顶部包含了一个编译器定义。取消注释该编译器定义,该文件中的所有静态方法都将变为公共。

正如我之前所说,有很多方法可以解决这个问题。

辅助方法

本节提供的信息对于理解/使用 `DAL` 对象不是必需的,仅为完整性文档提供。

方法 - `[protected virtual void Init()]`

我预计需要创建重载的构造函数,所以我决定将初始化代码放入一个可以从每个构造函数调用的方法中。结果是我还没有找到需要这样做的方法,但我把它保留了下来,因为你永远不知道将来会发生什么。因为它是一个虚拟方法,所以程序员可以轻松地重写它来执行额外的(BLL 特定的)初始化任务。

/// <summary>
/// Set some reasonable defaults
/// </summary>
protected virtual void Init()
{
    // this method is called from the constructor(s), and exists so that we can overload 
    // the constructor without duplicating code. 
    this.TimeoutSecs             = 300;
    this.FailOnMismatch          = false;
    this.AddReturnParamIfMissing = true;
    this.ExternalTransaction     = null;
    this.BulkInsertBatchSize     = 250;
    this.BulkCopyTimeout         = 600;
    this.BulkCopyOptions         = SqlBulkCopyOptions.Default;
    this.CanReadWriteDB          = true;
}

方法 - `[protected virtual string TestConnection()]`

添加一个方法来测试连接似乎很合乎逻辑。有趣的是,`DAL` 本身并不使用它,因为我认为程序员只想调用一次,从他的 BLL 对象(继承了 `DAL` 对象)中调用它。

/// <summary>
/// Allows the programmer to test the database connection before trying to use it.
/// </summary>
/// <returns>If valid and empty string, otherwise, the message text from the ensuing <br/>exception.</returns>
protected virtual string TestConnection()
{
    // assume success
    string        result = string.Empty;
    SqlConnection conn   = null;
    try
    {
        using (conn = new SqlConnection(this.ConnectionString.Base64Decode()))
        {
            conn.Open();
            conn.Close();
        }
    }
    catch (Exception ex)
    {
        result = ex.Message;
    }
    return result;
}

方法 - `[protected virtual string AddTryCatchTranToQuery(...)]`

如果您使用查询文本(而不是存储过程)来访问数据库,您可以调用此方法来用事务包装您的查询。它将您的查询包装在 try/catch 块中,并创建/提交一个事务。如果抛出异常,事务将被回滚。您还可以选择指定执行日志记录(或在查询失败时执行任何其他操作)的查询文本。

对于存储过程,我假设您已经包含了这类代码,尤其是在执行任何 CRUD 操作时。如果您不这样做,您应该这样做。

/// <summary>
/// Adds a try/catch block, as well as a transaction (with optional name) to the specified <br/>
/// plain sql query text. This code does not check to see if transaction code is already <br/>
/// part of the query.
/// </summary>
/// <param name="query">The query to encase</param>
/// <param name="logQuery">The query that implements your logging mechanism.</param>
/// <param name="transactionName">The desired transaction name (optional)</param>
/// <returns>The transaction-wrapped query</returns>
protected virtual string AddTryCatchTranToQuery(string query, string logQuery, string transactionName="")
{
    transactionName    = transactionName.Trim();
    logQuery           = logQuery.Trim();

    StringBuilder text = new StringBuilder();
    text.AppendLine("BEGIN TRY");
    text.AppendFormat("    BEGIN TRAN {0};", transactionName).AppendLine();
    text.AppendLine(query).AppendLine();
    text.AppendFormat("    COMMIT TRAN {0};", transactionName).AppendLine();
    text.AppendLine("END TRY");
    text.AppendLine("BEGIN CATCH");
    text.AppendFormat("    IF @@TRANCOUNT > 0 ROLLBACK TRAN {0};", transactionName).AppendLine();
    text.AppendLine(logQuery);
    text.AppendLine("END CATCH");
    return text.ToString();
}

方法 - `[protected virtual string NormalizeTableName(...)]`

`ExecuteBulkInsert` 方法的一个重载需要指定表名。为了防止 SQL 注入的可能性,我们需要通过在表名周围加上方括号来规范化指定的表名(它也处理了包含模式的名称。其思想是,任何放入方括号中的内容都将被评估为 SQL 表名,如果表名无效,则存储过程/查询将抛出 SQL 异常。

除了这种保护之外,您真的应该阅读这篇关于“SQL 注入”的文章。作者列出了缓解威胁的方法。如果您重视数据,这一点非常重要 - SQL 注入攻击以及防止它们的一些技巧 [^]。保护您的 SQL 完全由您负责,而且超出了本文的范围。

/// <summary>
/// Normalizes the table name so there's no chance of sql injection. You can't be too careful.
/// </summary>
/// <param name="tableName">The table name (should have the schema included as well.</param>
/// <returns>The tablename with square brackets around it</returns>
protected virtual string NormalizeTableName(string tableName)
{
    string[] parts = tableName.Split('.');
    tableName = string.Empty;
    foreach(string part in parts)
    {
        tableName = (string.IsNullOrEmpty(tableName))
                    ? string.Format("[{0}]", part)
                    : string.Format(".[{0}]", part);
    }
    return tableName.Replace("[[", "[").Replace("]]","]");
}

方法 - `[protected virtual int BulkInsertTargetCount(...)]`

ADO 的 `SqlBulkCopy` 对象会接收整个数据表,并将行插入数据库。不幸的是,它*不会*告诉您实际插入了多少条记录。如果您想知道这个值,您必须自己处理。此方法计算指定表中的记录数,并返回该值。`DAL` 在使用 `SqlBulkCopy` 对象之前和之后调用此方法,并返回两次计数之间的差值。

/// <summary>
/// Counts the number of records currently in the table targeted by the merge attempt.
/// </summary>
/// <param name="tableName">The table we're counting</param>
/// <returns>The number of records in the specified table</returns>
protected virtual int BulkInsertTargetCount(string tableName)
{
	if (string.IsNullOrEmpty(tableName))
	{
		throw new ArgumentNullException("tableName");
	}
	if (!tableName.Contains('.') || tableName.StartsWith("."))
	{
		throw new InvalidOperationException("The [tableName] must include a schema. Example: 'dbo.tableName'");
	}

	int result = 0;
	string query = string.Format("SELECT COUNT(1) FROM {0}", this.NormalizeTableName(tableName));
	List<EntityRowCount> rowCount = this.ExecuteQuery<EntityRowCount>(query);
	if (rowCount != null && rowCount.Count > 0) 
	{
		result = rowCount[0].Count;
	}
	return result;
}

方法 - `[protected virtual DataTable GetData(...)]`

归根结底,获取数据非常简单。您只需要执行的 SQL 查询,要传递给查询的参数,以及查询的类型(存储过程或 SQL 查询文本)。在我工作的公司,我们不使用任何直接的 SQL 查询,并要求所有数据库访问都通过存储过程完成,但我怀疑至少有一些环境不强制执行这种范式(那些地方可能像地狱一样可怕,并且注定所有敢于进入这种领域的人都将灭亡)。

对于这个 `DAL`,所有“获取数据”请求最终都会汇入 `GetData()` 方法。实际上并没有什么奇怪的事情发生。

/// <summary>
/// Calls SqlCommand.ExecuteDataReader() to retrieve a dataset from the database.
/// </summary>
/// <param name="cmdText">The storedproc or query to execute</param>
/// <param name="parameters">The parameters to use in the storedproc/query</param>
/// <returns>A DataTable object</returns>
/// <exception cref="ArgumentNullException">The cmdText parameter cannot be null/empty.</exception>
protected virtual DataTable GetData(string cmdText, 
                                    SqlParameter[] parameters=null, 
                                    CommandType cmdType = CommandType.StoredProcedure)
{
    if (string.IsNullOrEmpty(cmdText))
    {
        throw new ArgumentNullException("cmdText");
    }

    // by defining these variables OUTSIDE the using statements, we can evaluate them in 
    // the debugger even when the using's go out of scope.
    SqlConnection conn   = null;
    SqlCommand    cmd    = null;
    SqlDataReader reader = null;
    DataTable     data   = null;

    // create the connection
    using (conn = new SqlConnection(this.ConnectionString.Base64Decode()))
    {
        // open it
        conn.Open();
        // create the SqlCommand object
        using (cmd = new SqlCommand(cmdText, conn) { CommandTimeout = this.TimeoutSecs, CommandType = cmdType } )
        {
            // give the SqlCommand object the parameters required for the stored proc/query
            if (parameters != null)
            {
                cmd.Parameters.AddRange(parameters);
            }
            //create the SqlDataReader
            if (this.CanReadWriteDB)
            {
                using (reader = cmd.ExecuteReader())
                {
                    // move the data to a DataTable
                    data = new DataTable();
                    data.Load(reader);
                }
            }
        }
    }
    // return the DataTable object to the calling method
    return data;
}

方法 - `[protected virtual int SetData(...)]`

设置数据的实际操作只稍微有趣一点。除了让 `GetData` 完成其魔力所需的相同参数集外,`SetData` 还提供了一个可选参数,允许调用方法指示代码包含事务支持。

/// <summary>
/// Calls SqlCommand.ExecuteNonQuery to save data to the database.
/// </summary>
/// <param name="cmdText">The query text to execute</param>
/// <param name="parameters">The parameters to use</param>
/// <param name="cmdType">The sql command type</param>
/// <param name="useAdoTransaction">Flag indicating to wrap query with ado transaction</param>
/// <returns>The number of records affected</returns>
/// <exception cref="ArgumentNullException">The cmdText parameter cannot be null/empty.</exception>
protected virtual int SetData(string cmdText, 
								SqlParameter[] parameters, 
								CommandType cmdType = CommandType.StoredProcedure, 
								bool useAdoTransaction=false)
{
	if (string.IsNullOrEmpty(cmdText))
	{
		throw new ArgumentNullException("cmdText");
	}

	int result = 0;
	SqlConnection conn   = null;
	SqlCommand    cmd    =  null;
	SqlTransaction transaction = null;
	using (conn = new SqlConnection(this.ConnectionString.Base64Decode()))
	{
		conn.Open();
		if (useAdoTransaction && cmdType != CommandType.StoredProcedure)
		{
			transaction = conn.BeginTransaction();
		}

		using (cmd = new SqlCommand(cmdText, conn) { CommandTimeout = this.TimeoutSecs, CommandType = cmdType } )
		{
			SqlParameter rowsAffected = null;
			if (parameters != null)
			{
				cmd.Parameters.AddRange(parameters);
				// if this is a stored proc and we want to add a return param
				if (cmdType == CommandType.StoredProcedure && this.AddReturnParamIfMissing)
				{
					// see if we already have a return parameter
					rowsAffected = parameters.FirstOrDefault(x=>x.Direction == ParameterDirection.ReturnValue);
					// if we don't, add one.
					if (rowsAffected == null)
					{
						rowsAffected = cmd.Parameters.Add(new SqlParameter("@rowsAffected", SqlDbType.Int) { Direction = ParameterDirection.ReturnValue } );
					}
				}
			}
			try
			{
				if (this.CanReadWriteDB)
				{
					result = cmd.ExecuteNonQuery();
				}
			}
			catch (SqlException ex)
			{
				if (transaction != null && cmdType != CommandType.StoredProcedure)
				{
					transaction.Rollback();
				}
				throw (ex);
			}
			result = (rowsAffected != null) ? (int)rowsAffected.Value : result;
		}
	}
	return result;
}

方法 - `[protected virtual int DoBulkMerge(...)]`

虽然 ADO 的 `SqlBulkCopy` 对象是一种将数据快速插入数据库的好方法,但它缺乏有时所需的精细度,即根据目标表的用途选择插入或更新记录的能力。因此,`DAL` 对象提供了一个 `ExecuteStoredProc`(和 `ExecuteQuery`)方法重载来实现这种典型的“upsert”功能。我们已经讨论了重载方法,它们都调用此方法。

在其中,创建了一个持久化的 `SqlConnection` 对象,以便在插入多行数据时提供一定程度的效率。除此之外,它与 `SetData` 方法(也已讨论过)非常相似。

强烈建议使用存储过程来进行 upsert 操作。

/// <summary>
/// Base BulkMerge 
/// </summary>
/// <typeparam name="T">The entity type represented by the collection</typeparam>
/// <param name="data">The entity collection</param>
/// <param name="queryText">The query text to execute</param>
/// <param name="bulkType">How to extract propertiues from the entity</param>
/// <param name="cmdType">The sql command type</param>
/// <returns>Number of records affected.</returns>
protected virtual int DoBulkMerge<T>(IEnumerable<T> data, 
                                        string queryText, 
                                        BulkInsertType bulkType, 
                                        CommandType cmdType, 
                                        ParamPrecedence precedence = ParamPrecedence.None,
                                        string paramArrayPropName = "SqlParameters",
                                        bool useAdoTransaction=false)
{
    if (string.IsNullOrEmpty(queryText))
    {
        throw new ArgumentNullException("queryText");
    }

    int result = 0;
    SqlConnection  conn        = null;
    SqlCommand     cmd         = null;
    SqlTransaction transaction = null;
    using (conn = new SqlConnection(this.ConnectionString.Base64Decode()))
    {
        conn.Open();
        if (useAdoTransaction && cmdType != CommandType.StoredProcedure)
        {
            transaction = conn.BeginTransaction();
        }
        using (cmd = new SqlCommand(queryText, conn) { CommandTimeout = this.TimeoutSecs, CommandType = cmdType } )
        {
            try
            {
                foreach(T item in data)
                {
                    SqlParameter[] parameters = DAL.MakeSqlParameters(item, bulkType, precedence, paramArrayPropName);
                    if (parameters != null)
                    {
                        cmd.Parameters.AddRange(parameters);
                        if (this.CanReadWriteDB)
                        {
                            cmd.ExecuteNonQuery();
                        }
                        cmd.Parameters.Clear();
                        result++;
                    }
                }
            }
            catch (Exception ex)
            {
                if (transaction != null)
                {
                    transaction.Rollback();
                }
                throw(ex);
            }
        }
    }
    return result;
}

这是一个 upsert 样式存储过程的示例。请记住,此代码*并非*防止重复项插入表的灵丹妙药。您应该实施约束来强制执行唯一记录(Google 是您的朋友),并且此外,您可以使用 SQL `MERGE` 语句而不是下面提供的简单 upsert 代码。

DECLARE @rowCount INT = -1;

BEGIN TRY

    BEGIN TRANSACTION MergeData_Transaction;

    UPDATE	dbo.MyTable
    SET		StringValue     = @StringValue   
            ,IntValue		= @IntValue     
    WHERE	IntValue		= @IntValue;
    SET @rowCount = @@ROWCOUNT;

    IF @rowCount = 0
    BEGIN
        INSERT INTO dbo.WebSvcTarget
        (
            StringValue
            ,IntValue
        )
        VALUES
        (
            @StringValue   
            ,@IntValue     
        )
    END
    SET @rowCount = @rowCount + @@ROWCOUNT;

    COMMIT TRANSACTION MergeData_Transaction;

END TRY
BEGIN CATCH
    IF @@TRANCOUNT > 0
    BEGIN
        SET @rowCount = 0;
        ROLLBACK TRANSACTION MergeData_Transaction;

        -- do something with the error if desired
    END
END CATCH

方法 - `[protected static T ConvertFromDBValue<T>(...)`

此方法支持我们在 `DAL` 对象中非常依赖泛型类型。它的目的是将从数据库检索到的值转换为 C# 在填充 `DataTable` 时可以处理的值。

/// <summary>
/// Converts a value from its database value to something we can use (we need this because <br/>
/// we're using reflection to populate our entities)
/// </summary>
/// <typeparam name="T">The object type</typeparam>
/// <param name="obj">The object</param>
/// <param name="defaultValue">The default value to be used if object is null</param>
/// <returns>The object of the associated C# data type</returns>
protected static T ConvertFromDBValue<T>(object obj, T defaultValue)
{
    T result = (obj == null || obj == DBNull.Value) ? default(T) : (T)obj;
    return result;
}

方法 - `[protected virtual List<T> MakeEntityFromDataTable<T>(...)]`

 

/// <summary>
/// Creates the list of entities from the specified DataTable object. We do this because we <br/>
/// have two methods that both need to do the same thing.
/// </summary>
/// <typeparam name="T">The entity type represented by the collection</typeparam>
/// <param name="data">The entity collection</param>
/// <returns>The instantiated and populated list of entities.</returns>
/// <exception cref="ArgumentNullException">The data parameter cannot be null.</exception>
protected virtual List<T> MakeEntityFromDataTable<T>(DataTable data)
{
    if (data == null)
    {
        throw new ArgumentNullException("data");
    }

    //----------------------------

    Type objType = typeof(T);
    List<T> collection = new List<T>();
    // if we got back data
    if (data != null && data.Rows.Count > 0)
    {
        // we're going to count how many properties in the model were assigned from the 
        // datatable.
        int matched = 0;

        foreach(DataRow row in data.Rows)
        {
            // create an instance of our object
            T item = (T)Activator.CreateInstance(objType);

            // get our object type's properties
            PropertyInfo[] properties = objType.GetProperties();

            // set the object's properties as they are found.
            foreach (PropertyInfo property in properties)
            {
                if (data.Columns.Contains(property.Name))
                {
                    Type pType       = property.PropertyType;
                    var defaultValue = pType.GetDefaultValue();
                    var value        = row[property.Name];
                    value            = DAL.ConvertFromDBValue(value, defaultValue );
                    property.SetValue(item, value);
                    matched++;
                }
            }
            if (matched != data.Columns.Count && this.FailOnMismatch)
            {
                throw new Exception("Data retrieved does not match specified model.");
            }
            collection.Add(item);
        }
    }
    return collection;
}

方法 - `[protected virtual DataTable MakeDataTable<T>(...)]`

此方法从指定的集合创建 `DataTable` 对象。列是自动创建的,并且行是根据集合实体的定义添加的。此方法由前面讨论的大多数负责将数据*放入*数据库的方法调用。

/// <summary>
/// Creates a DataTable and populates it with the specified data collection.
/// </summary>
/// <typeparam name="T">The entity type represented by the collection</typeparam>
/// <param name="data">The collection of entties</param>
/// <param name="tableName">The name of the table to insert the data into</param>
/// <param name="bulkType">How to populate the columns from the entity</param>
/// <returns>The created/populated DataTable object.</returns>
protected virtual DataTable MakeDataTable<T>(IEnumerable<T> data, 
                                                string tableName, 
                                                BulkInsertType bulkType, 
                                                ParamPrecedence precedence = ParamPrecedence.None, 
                                                string paramArrayPropName = "SqlParameters")
{
    DataTable dataTable = null;

    Debug.WriteLine(string.Format("MakeDataTable - {0} data items specified.", data.Count()));

    using (dataTable = new DataTable(){TableName = tableName })
    {
        Type type = typeof(T);

        // Get the properties to send to the database. If byDBInsertAttribute is true, only object 
        // properties that are decorated with the CanDBInsert attribute will be retrieved. If 
        // byDBInsertAttribute is false, only properties that have a Set method will be retrieved
        PropertyInfo[] properties = DAL.GetEntityProperties(type, bulkType);

        Debug.WriteLine(string.Format("MakeDataTable - {0} item properties per item.", properties.Length));

        // create columns that match the retrieved properties
        foreach (PropertyInfo property in properties)
        {
            dataTable.Columns.Add(new DataColumn(property.Name, property.PropertyType));
        }

        Debug.WriteLine(string.Format("MakeDataTable - {0} dataTable columns created.", dataTable.Columns.Count));

        // add the rows to the datatable
        foreach (T entity in data)
        {
            DataRow row = dataTable.NewRow();
            foreach (PropertyInfo property in properties)
            {
                row[property.Name] = property.GetValue(entity);
            }
            dataTable.Rows.Add(row);
        }
    }

    Debug.WriteLine(string.Format("MakeDataTable - {0} rows created.", dataTable.Rows.Count));

    return dataTable;
}

TestHarness 应用程序

没有测试载体,库算什么?在此解决方案中,TestHarness 是一个控制台应用程序,它允许您在将 `DAL` 对象放入自己的代码中并感到满意之前进行尝试。如呈现的,该应用程序演示了我疯狂的以下方面:

使用单例模式

在使用原始 `PWConnectionString` 类时,一位程序员建议最好能创建一个全局可用的实例。我建议他创建一个 `static Globals` 类来做到这一点,但在思考了一会儿之后,我认为我可以提供一个线程安全的单例容器类,它几乎可以做到同样的事情,而无需创建前面提到的静态 `Global` 类。

这个单例容器简单地允许程序员实例化类一次,并在整个应用程序中使用该实例。对于不熟悉单例模式的人,我在这里找到了一个不错的解释 - 在 C# 中实现单例模式 [^]。

[Singleton<T>] 基类

这是我第一次真正接触单例模式,所以,我做的很多事情是为了“让它工作”完全不必要的,但我喜欢尝试我正在学习的新东西。为了进行实验,我决定看看我是否可以从单例实现中提取出少量不必要的复杂性,到一个基类中,然后可以在面向外部的代码中继承它。在我寻找我想做的示例时,我发现了这篇 CodeProject 文章 - C# 中单例模式的可重用基类 [^],作者是 Boris Brock。

单例对象类应该是 `sealed` 以防止继承,但在这种情况下,我们*希望*继承它,但同时又*不希望*它本身被实例化,所以我们没有使用 `sealed`,而是将类设为 `abstract`。此外,单例类有一个私有构造函数,但同样,我们需要使其构造函数可访问,以便我们可以继承它,因此我们将构造函数的访问修饰符更改为 `protected`。在完成这些之后,我们就可以使用泛型类型来使其易于任何派生类消化。

/// <summary>
/// Represents the singleton base class.
/// </summary>
public abstract class Singleton<T> where T : class
{
    // The class is abstract so you can't instatiate it. Classes that derive 
    // from this class should be sealed.

    // .Net 4+ Lazy object
    private static readonly Lazy<T> lazy = new Lazy<T>(()=>Create());

    public static T Instance { get { return lazy.Value; } }

    // Protected constructor makes it possible to inherit this class
    protected Singleton()
    {
        Debug.WriteLine(string.Format("{0} instantiated.", typeof(T).GetType().Name));
    }

    private static T Create()
    {
        return Activator.CreateInstance(typeof(T), true) as T;
    }
}

[PWConnectionStringleton]

懂了吗?“Stringleton”?听起来像“Singleton”?我逗笑了自己!

我将我的单例视为实际实例化对象的容器。这使得单例模式本身与实例化对象隔离,从而以很少/没有代码重复的方式使代码更加灵活(可以使用或*不使用*单例)。

正如您所见,派生的单例类看起来就像一个常规类。由于单例对象的构造函数不能接受参数,因此我们必须包含一个允许我们初始化包含对象的机制,即 `PWConnectionString` 对象。

/// <summary>
/// Represents the singleton container the PWConnectString class.
/// </summary>
public sealed partial class PWConnectionStringleton:Singleton<PWConnectionStringleton>
{
    public PWConnectionString PWConnString { get; set; }

    public void Init(string name, string server, string database, bool encryptTraffic=false, string uid="", string pwd="")
    {
        this.PWConnString =  new PWConnectionString(name, server, database, encryptTraffic, uid, pwd);
    }
}

[BLLSingleton]

[BLL]

`DAL` 旨在被您的 BLL 对象继承。如果您对 .Net 和面向对象开发有任何经验,您就会明白这一切是如何工作的。由于大多数方法和属性都是 `protected` 的,因此它们只能从 `DAL` 本身或从继承自它的类中使用。您希望简单继承 `DAL` 的原因是,您可以扩展它自己的方法,提供供外部代码使用的公共方法,并避免用您添加的代码污染 `DAL`。

继承 `[DAL]` 对象

下面您将看到如何实现。为了在此应用程序中进行测试,我们通过将 `DAL.CanReadWriteDB` 属性设置为 false 来阻止 `DAL` 实际从数据库读取或写入。这将允许我们测试 `DAL` 的各种方面,而无需执行存储过程或查询。

public partial class BLL : DAL
{
    public BLL(PWConnectionString connStr) : base(connStr)
    {
        // for the purposes of testing
        this.CanReadWriteDB = false;
    }

    //... more code that you write...
}

您可能编写的一个方法的示例是检索数据并将其存储到您定义的实体集合中。

// get some data
public class BLL:DAL
{
    ...

   public List<MyObjects> GetSalesData()
   {
       List<MyObjects> list = this.ExecuteStoredProc("dbo.GetSalesData", null);
       return list;
   }

   ...
}

// calling it would look like this
List<MyObjects> list = BLL.GetSalesData();

……或者一个将数据保存到数据库的方法(记住,此方法有多个重载使其成为可能)

// save some data
public class BLL:DAL
{
    ...

    public int SaveSalesData(MyObject model)
    {
        int recordsAffected = this.ExecuteStoredProc(model);
        return recordsAffected;
    }
    public int SaveSalesDataCollection(List<MyObject> model)
    {
        int recordsAffected = this.ExecuteStoredProc(model.AsEnumerable());
        return recordsAffected;
    }

    ...
}

//---------------------------------------------------
// given the following (this code goes in the object that 
// is calling the BLL methods)
List<MyObject> myObjects = new List<MyObject>(){new MyObject(),new MyObject(),...};
// calling it would look like this
int result = BLL.SaveSalesData(myObject[0]);
// or
result = BLL.SaveSalesDataCollection(myObjects);

而且不要忘记批量插入的能力

public class BLL:DAL
{
    ...

    public int BulkInsertSalesData(List<MyObject> model)
    {
        int recordsAffected = this.ExecuteBulkInsert(model, 
                                                     "sales", 
                                                     BulkInsertType.DBInsertAttribute);
        return recordsAffected;
    }

    public int UpsertSalesData(List<MyObject> model)
    {
        int recordsAffected = this.ExecuteStoredProc(model.AsEnumerable(), 
                                                     "storedprocname", 
                                                     BulkInsertType.DBInsertAttribute);
        return recordsAffected;
    }

    ...
}

//---------------------------------------------------
// given the following (this code goes in the object that 
// is calling the BLL methods)
List<MyObject> myObjects = new List<MyObject>(){new MyObject(),new MyObject(),...};
// calling it would look like this
int result = BLL.BulkInsertSalesData(myObjects);
// or
result = BLL.UpsertSalesData(myObjects);

我个人认为,您离面向外部的实现越近,接口就应该越简单。您可以在上面的代码示例中看到它是如何工作的。

[Program]

这就是魔法发生的地方。我们实例化了单例的 ConnectionString 和 BLL 对象,检查字符串值的含量,然后继续执行测试方法。请注意启用和禁用单例使用的编译器指令。请记住,如果您关闭单例,您也必须在 MyClass 对象中执行此操作。

//#undef __USE_SINGLETONS__
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PaddedwallDAL;
using ObjectExtensions;

namespace TestHarness
{
    // This project is only intended to demonstrate the use of the connection string and DAL 
    // classes as either instantiated on-demand objects, or as globally accessible singleton 
    // objects. Do NOT add this project to your own projects.
    class Program
    {
        static PWConnectionString PWConnString 
        { 
#if __USE_SINGLETONS__ 
            get { return PWConnectionStringleton.Instance.PWConnString; } 
#else
            get; set;
#endif
        }

        static BLL BLL
        {
#if __USE_SINGLETONS__
            get { return BLLSingleton.Instance.BLL; }
#else
            get; set;
#endif
        }

        static void Main(string[] args)
        {
            try
            {
                // this lets us write to the console
                Global.IsConsoleApp = true;

#if __USE_SINGLETONS__
                // if you want to use singletons (desktop apps only?) for your connection string 
                // and/or BLL, do this. We use the "master" database so you don't have to create 
                // a database when first exploring the code.
                PWConnectionStringleton.Instance.Init("Test", "localhost", "master");
                BLLSingleton.Instance.Init(PWConnectionStringleton.Instance.PWConnString);

                // see what the connection string contains
                Global.WriteLine(string.Format("ConnectionString (base64) = {0}", PWConnString.ConnectionString));
                Global.WriteLine(string.Format("ConnectionString (ascii)  = {0}", PWConnString.ConnectionString.Base64Decode()));

                // assuming you set up properties to access the BLL object (as demonstrated in this 
                // app), you can use this throughout the application:
                BLL.MyMethod();

                // or sinmply do this (but I think this is uglier and prone to typing mistakes):
                BLLSingleton.Instance.BLL.MyMethod();

                // testing access to the BLL singleton from another class. Put a breakpoint in 
                // this class constructr, and inspect the BLL object.
                MyClass myClass = new MyClass();

#else
                // we use a guid for the name to prove it's a uniquely created instance 
                PWConnString = new PWConnectionString(Guid.NewGuid().ToString(), "localhost", "Test");
                BLL = new BLL(PWConnString);
#endif

                // testing the property/parameter code - generates a crapload of console output
                BLL.TestEntity1();
                BLL.TestEntity2();
                BLL.TestEntity3();
            }
            catch (Exception ex)
            {
                Global.WriteLine(string.Format("{0}{1}{2}", ex.Message, Environment.NewLine, ex.StackTrace));
            }
            // gives you a chance to see the console output before dismissing the console window.
            Console.WriteLine();
            Console.WriteLine("Press a key...");
            Console.ReadKey();
        }
    }
}

其他说明

实例化连接字符串或 BLL 对象作为单例不是必需的,但我认为代码需要测试,所以我们在这里。对于那些想看使用单例和*不使用*单例之间区别的人,我在项目属性中创建了一个名为 `__USE_SINGLETONS__` 的编译器定义,它会自动告诉代码使用单例,但允许程序员通过简单地取消定义编译器定义(参见 program.cs 文件的顶部)来在两种范式之间切换。

结语

我试图尽可能地使 ADO 的使用通用化,因为我得支持我普遍不喜欢的 ORM。就像我父亲常说的:“如果你要说一套,那就确保你能做到。”

如果本文违反了您扭曲的“最佳实践”观念,那也是您自己的事。但是,不要觉得您不能提出您认为我走错了路的地方,以及整个社会因此损失了多少,或者某些受保护的个人群体很可能会坠入地狱。

重大变更 - .Net Core 转换版本

我先说明一下,这*仅仅*是出于好奇而做的,我*绝不*提倡使用 .Net Core。事实上,在简短接触 VS2019 和整个 Core 范式之后,我*并不* impressed。话虽如此,我们开始吧……

首先

注意: 如果您不想自己转换,我在本文顶部提供了一个新下载版本(包含此版本)。

此代码使用 .Net Core 3.1(截至撰写本文时,处于“预发布”阶段——您自己体会吧),并且是在 VS2019 中创建的(我不知道 Core 3.1 是否适用于 VS2017)。请记住,除了少数 `using` 语句的更改外,*没有任何*实际的类代码需要更改以支持 Core。所以,如果您准备好了,这是我所做的:

  • 创建一个新的 .Core 类库项目,名为 **PaddedwallDalCore**,并删除默认的 `Class1.cs` 文件。
     
  • 向项目添加一个新的 .Core 控制台应用程序,名为 **TestHarness**,并将其设置为“启动项目”。
     
  • 在控制台应用程序项目中添加对类库项目的引用。
     
  • 使用 NuGet 将 **Microsoft.Data.SqlClient** 添加到解决方案中。
     
  • 从原始 **PaddedwallDAL** 解决方案中,将以下文件夹/文件复制到相应的解决方案项目中。对于那些没有注意到的人,VS2019 不需要您显示隐藏文件并对刚复制的文件夹/文件执行“包含到项目”操作。复制后,它们会自动添加到项目中(并且您仍然可以选择排除它们,就像在早期版本的 Visual Studio 中一样)。
     
  • 在整个解决方案中,“在文件中替换”所有 `using System.Data.SqlClient` 的实例为 `Microsoft.Data.SqlClient`。
     

除非我遗漏了某个步骤,否则以上所有操作大约需要 15 分钟。编译/运行后,它的行为与 .Net Framework 版本完全相同。

.Net Core 中关于 ADO 的评论

恕我直言,这似乎有些半吊子。它们没有将所有内容都整合到 Core 的 `Microsoft` 命名空间中,而是要求您继续使用 `System.Data`。不仅如此,Core 的 ADO 支持仍处于预发布阶段,并且它们在更改——或者更糟的是,删除——Core 中的内容(而不告知任何人)。恕我直言,这*并不*能促进其在企业级代码中的快速采用,也*不*提倡使用它。

最后,我经常听到“跨平台”这个词与 Core 相关联。在 Linux/iOS 上,没有 Mono,您的 Core 代码*将*无法运行。这使得它与 .Net Framework 相同,因此,*不是*真正意义上的跨平台。

所以,如果您想做 Core,我猜您可以,但直到它*稳定*——并且已经稳定了至少一年——我才这样做。不感冒……

关注点

如果您想看一个如何扩展此代码的示例,请访问这篇文章 - 通用 DAL 重访 - 让它成为你自己的,实际示例 [^]。

为什么他们因为冠状病毒而释放危险的重刑犯,却因为违反居家令而逮捕人们?这些事情真让人费解。

历史

  • 2020.03.06 添加了 Core 3.1 版本。
  • 2020.03.05 初始发布。
     
© . All rights reserved.