It is a bit difficult to create a good data hierarchy in ini files. This is part of why MS seems to have migrated mostly to XML files. See the answer to this question: Reading/writing an INI file
If you go with the XML option, I'd skip this mapping stuff and simply serialize your objects directly in after using XPath to find the appropriate XML. Then you don't need a mapper.
You could also go with an in-memory or file-based DB, like SqLite. Perf will be great, and you will have a very small deployment footprint.
Also, I recommend avoiding trying to abstract this mapper stuff, since I don't think it will translate well between a DB and an ini file. If you look at the complexity of the many ORM libraries out there, you will see how difficult this mapping really can be. Most of the concepts at the mapping level simply don't translate well to an ini file. It is the higher level concepts that will map (repositories), which is why I posted my original answer (see below).
But if you want to keep with the pattern you're using, and your ini file looks something like this:
[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
... then you could simply write your repository to grab data out of ini sections, and write your mappers to look at each ini value the same way you are currently looking at columns in your result set.
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);
// ...
}
}
Your current mapper code is bound to your storage type, so you'll need to come up with a more generic mapper interface. Or make that last reader parameter more generic to support both mapping types (in your case, reader
, in my case, each ini section instance). 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));
}
}
Note that this is all custom code - there is no official ini file reader, and I haven't tried any out so I can't make a suggestion on which third party library to use. That question I linked at the top has some recommendations.
Original answer
(part of it may still be useful)
Make an interface
for your repository, and make sure the higher level layers of your code only talk to your data store through this interface.
An example interface (yours might be different):
public interface IReportRepository
{
void Create(Report report);
Report Read(int id);
void Update(Report report);
void Delete(Report report);
}
You could also make this interface generic, if you wanted.
To make sure the higher level layers only know about the repository, you could construct the classes for talking to the file/DB in the implementation of IReportRepository
, or use Dependency Injection to populate it. But whatever you do, don't let your higher level code know about anything but IRepository
and your individual data entities (Report
).
You might also want to look into the Unit of Work pattern, and wrap the actual data access there. That way you can easily support transactional semantics, and buffered/lazy storage access (even with a file).
For your example implementation, the SqlConnection
and SqlDataReader
would live in your unit of work class, and the mapping code and specific stored procedure names would probably live in each repository class.
It might be a bit tricky to get this structure to work completely independently, but if you look at the code the Microsoft Entity Framework generates, they actually have their unit of work class instantiate each repository, and you just access it like a property. Something roughly like:
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
}
Useful reading: