存储在ini文件中的信息有什么好的Mapper模式?

时间:2011-10-03 20:31:02

标签: c# design-patterns

在设计n层应用程序时,我倾向于使用从Lhotka的CSLA框架中采用和改编的模式。简而言之,Repository层填充SqlDataReader并将数据读取器,要映射的实例和映射信息传递给Mapper层,然后Mapper层填充实例。

这种模式在我参与的许多项目中一次又一次地证明了这一点:

  1. 易于理解和实施,开发人员倾向于坚持,因为他们了解它在做什么。
  2. 它可以非常方便地重用继承类的映射器以及复合类。
  3. 实体中的任何更改以及分配给它的映射器都会导致编译时错误直接指向也需要更改的存储过程。
  4. 以下是一些示例存储库代码:

    internal static List<CompositeEntities.ContactReportRpa> RetrieveByReportId(int reportId)
    {
      CompositeEntities.ContactReportRpa report = null;
      List<CompositeEntities.ContactReportRpa> reports = new List<CompositeEntities.ContactReportRpa>();
      using (SqlConnection conn = DBConnection.GetConnection())
      {
        cmd.Connection = conn;
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        cmd.CommandText = "ContactReportRpa_SEL_ByIdReport";
        cmd.Parameters.Add("@IdReport", System.Data.SqlDbType.Int).Value = reportId;
        conn.Open();
        using (SqlDataReader reader = cmd.ExecuteReader())
        {
         while (reader.Read())
         {
           report = new CompositeEntities.ContactReportRpa();
           ContactReportRpaMapper.Map("IdReportRpa", "IdReport", "IdRecommendation", "IsDisplayed", "Comments", report.Rpa, reader);
           RpaRecommendationMapper.Map("IdRecommendation", "IdDepartment", "TitleRecommendation", "Description", "DisplayOrderRecommendation", report.Recommendation, reader);
           RpaDepartmentMapper.Map("IdDepartment", "TitleDepartment", "DisplayOrderDepartment", report.Department, reader);
           reports.Add(report);
         }
        }
       }
       return reports;
     }
    

    以下是一些Mapper Code示例。它非常简单:映射器知道哪个类属性被映射到数据读取器中的每个字段。每个字段的名称都会传递给映射器,因此无论分配给sproc中每个字段的名称如何,都可以使用相同的映射器。

    internal static void Map(string fieldId, string fieldName, string fieldDisplayOrder,   RpaDepartment entity, SqlDataReader reader)
    {
      entity.Id = reader.GetInt32(reader.GetOrdinal(fieldId));
      entity.Title = reader.GetString(reader.GetOrdinal(fieldName));
      entity.DisplayOrder = reader.GetInt32(reader.GetOrdinal(fieldDisplayOrder));
    }
    

    所以我的问题是:当数据源是文本文件时,我应该如何实现这种模式?我想坚持使用这种模式,因为最终数据源将迁移到数据库。

    有什么建议吗?

    编辑:ini文件已存在,此时我没有继续更改它们。所以我暂时坚持使用它们。

2 个答案:

答案 0 :(得分:3)

在ini文件中创建良好的数据层次结构有点困难。这是MS似乎主要迁移到XML文件的部分原因。请参阅此问题的答案:Reading/writing an INI file

如果你使用XML选项,我会跳过这个映射的东西,并在使用XPath之后直接序列化你的对象以找到适当的XML。那你就不需要映射器了。

您也可以使用an in-memory or file-based DB, like SqLite。 Perf会很棒,而且你的部署空间非常小。

另外,我建议避免尝试抽象这个映射器的东西,因为我认为它不会在DB和ini文件之间很好地转换。如果你看一下那里的许多ORM库的复杂性,你会发现这个映射真的很难。映射级别的大多数概念都不能很好地转换为ini文件。它是映射(存储库)的更高级别概念,这就是我发布原始答案的原因(见下文)。

但是如果你想保留你正在使用的模式,你的ini文件看起来像这样:

[Report.3]
IdReport = 3
IdReportRpas = 7,13

[ReportRpa.7]
IdReportRpa = 7
IdReport = 3
IdRecommendation = 12
IsDisplayed = true
Comments = I'm not sure what an RPA is...

[ReportRpa.13]
IdReportRpa = 13
IdReport = 3
; ... and rest of properties here

[Recommendation.12]
IdRecommendation = 12
IdDepartment = 33
TitleRecommendation = Some Recommendation
Description = Some Recommendation Description
DisplayOrderRecommendation = 0

[Department.33]
IdDepartment = 33
TitleDepartment = Bureau of DBs and ini files
DisplayOrderDepartment = 0

...然后您可以简单地编写存储库以从ini部分中获取数据,并编写映射器以查看每个ini值,就像您当前查看结果集中的列一样。

using(var iniFileReader = new IniFileReader())
{
    string reportSectionName = string.Format("Report.{0}", contactId);
    var reportSection = iniFileReader.GetSection(reportSectionName);

    // Todo: Abstract this sort of procedure/enumeration stuff out.
    // Similar to the existing code's stored procedure call
    int[] idReportRpas = reportSection.GetValue(IdReportRpas)
        .Split(',')
        .Select(s => int.Parse(s);

    foreach(string idReportRpa in idReportRpas)
    {
        report = new CompositeEntities.ContactReportRpa();

        string rpaSectionName = string.Format("ReportRpa.{0}", idReportRpa);
        var rpaSection = iniFileReader.GetSection(rpaSectionName);

        ContactReportRpaMapper.Map("IdReportRpa", "IdReport", "IdRecommendation",
            "IsDisplayed", "Comments", report.Rpa, rpaSection);

        // ...
    }
}

您当前的映射器代码绑定到您的存储类型,因此您需要提供更通用的映射器接口。或者使最后一个读者参数更通用以支持两种映射类型(在您的情况下,reader,在我的情况下,每个ini部分实例)。 E.g:

public interface IDataValueReader
{
    // Signature is one that might be able to support ini files:
    // string -> string; then cast
    //
    // As well as a DB reader:
    // string -> strongly typed object
    T ReadValue<T>(string valueName);
}

public class DbDataReader : IDataValueReader
{
    private readonly SqlDataReader reader;

    public DbDataReader(SqlDataReader reader)
    {
        this.reader = reader;
    }

    object ReadValue<T>(string fieldId)
    {
        return (T)reader.GetObject(reader.GetOrdinal(fieldId));
    }
}

public class IniDataSectionReader : IDataValueReader
{
    private readonly IniFileSection fileSection;

    public IniDataSectionReader(IniFileSection fileSection)
    {
        this.fileSection = fileSection;
    }

    object ReadValue<T>(string valueName)
    {
        return (T)Convert.ChangeType(fileSection.GetValue(fieldId), typeof(T));
    }
}

请注意,这是所有自定义代码 - 没有官方的ini文件阅读器,我还没有尝试过,所以我无法建议使用哪个第三方库。 That question I linked at the top有一些建议。

原始答案

(部分内容可能仍然有用)

repository制作interface,并确保代码的更高级别层只通过此界面与您的数据存储区通信。

示例界面(您的界面可能不同):

public interface IReportRepository
{
    void Create(Report report);
    Report Read(int id);
    void Update(Report report);
    void Delete(Report report);
}

如果您愿意,也可以将此界面设为通用。

要确保更高级别的层只知道存储库,您可以构建用于在IReportRepository的实现中与文件/ DB进行通信的类,或使用Dependency Injection来填充它。但无论你做什么,除了IRepository和你的个人数据实体(Report)之外,不要让你的更高级代码知道任何事情。

您可能还想查看the Unit of Work pattern,并在那里包装实际的数据访问权限。这样您就可以轻松支持transactional semantics和缓冲/延迟存储访问(即使使用文件)。

对于您的示例实现,SqlConnectionSqlDataReader将存在于您的工作单元类中,并且映射代码和特定存储过程名称可能存在于每个存储库类中。

让这个结构完全独立地工作可能有点棘手,但是如果你看一下Microsoft Entity Framework生成的代码,他们实际上让他们的工作单元实例化每个存储库,你只需要访问它就像一个属性。大致像:

public interface IUnitOfWork : IDisposable
{
    void CommitChanges();
    void RollbackChanges();
}

public class MyDataModel : IUnitOfWork
{
    private bool isDisposed;
    private readonly SqlConnection sqlConnection;

    public MyDataModel()
    {
        sqlConnection = DBConnection.GetConnection();
    }

    // Todo: Implement IUnitOfWork here

    public void Dispose()
    {
        sqlConnection.Dispose();
        isDisposed = true;
    }

    public IRepository<Report> Reports
    {
        get
        {
            return new ReportDbRepository(sqlConnection);
        }
    }
}

public class ReportDbRepository : IRepository<Report>
{
    private readonly SqlConnection sqlConnection;

    public ReportDbRepository(SqlConnection sqlConnection)
    {
        this.sqlConnection = sqlConnection;
    }

    // Todo: Implement IRepository<Report> here using sqlConnection
}

有用的阅读:

答案 1 :(得分:0)

您可以为INI文件实现相同的模式,尽管使用P / Invoke进行调用会有点麻烦。基本思想是调用GetPrivateProfileSectionNames以获取INI文件中的节名称列表。然后为每个部分名称调用GetPrivateProfileSection以获取该部分的键和值的列表。从那里你可以解析键和值并填充你的列表。

请参阅对Reading/writing an INI file的响应,以获取将读取INI文件的代码的指针。