What we are looking at is a long tutorial on Inversion of Control and dependency injection. Basically at the most simpilest form you will be looking to abstract your data layer from you application layer and provide interfaces for your IoC container to deliver to the application.
This can get quite confusing however all we have to do is remeber our application will be using the interfaces to work with the data layer and not the actual implementation.
So for your Unit of work you would resolve IUnityOfWork
interface instead. Now here is a quick example I put together. It is by no means a finished or tested product (and the xml side is very fragile) but shows how this can work.
First off I am using the interface IRepository<T>
to access the data layer. This can be connected database through EntityFramework or Xml through serialization. This repository is simple.
public partial interface IRepository<T>
where T : BaseEntity
{
T GetById(int id);
T Insert(T item);
T Update(T item);
void Delete(T item);
IQueryable<T> Entities { get; }
}
As simple interface the has the standard insert\update\delete and queryable methods. It is worth noting that I am referencing BaseEntity
which is a simple entity with an public int Id { get; set; } property
.
Now for Database connections I have created an implementation called EfRepository<T>
public class EfRepository<T> : IRepository<T>
where T : BaseEntity
{
public EfRepository(DbContext dbContext)
{
if (dbContext == null)
throw new ArgumentNullException("dbContext");
_dbContext = dbContext;
}
readonly DbContext _dbContext;
DbSet<T> _entities;
/// <summary>
/// Gets the entities as a querably
/// </summary>
public IQueryable<T> Entities { get { return DbEntities; } }
protected virtual DbSet<T> DbEntities { get { return _entities ?? (_entities = _dbContext.Set<T>()); } }
public void Delete(T item)
{
DbEntities.Remove(item);
_dbContext.SaveChanges();
}
public T GetById(int id)
{
return DbEntities.Find(id);
}
public T Insert(T item)
{
DbEntities.Add(item);
_dbContext.SaveChanges();
return item;
}
public T Update(T item)
{
_dbContext.SaveChanges();
return item;
}
}
Now this is a very simple class that connects to my Data layer via entity framework. (We will talk about the constructor shortly).
Next I have an Xml Repository named XmlRepository<T>
public class XmlRepository<T> : IRepository<T>
where T : BaseEntity
{
public XmlRepository(XmlContext xmlContext)
{
_xmlContext = xmlContext;
}
readonly XmlContext _xmlContext;
IList<T> _entities;
public IQueryable<T> Entities
{
get
{
return XmlEntities.AsQueryable();
}
}
protected IList<T> XmlEntities
{
get
{
return _entities ?? (_entities = _xmlContext.Set<T>());
}
}
public void Delete(T item)
{
if (XmlEntities.Any(i => i.Id == item.Id))
{
XmlEntities.Remove(item);
_xmlContext.SaveChanges(XmlEntities);
}
}
public T GetById(int id)
{
return Entities.FirstOrDefault(x => x.Id == id);
}
public T Insert(T item)
{
if (item.Id == default(int)) //only add if id = default(int)
{
var lastEntity = Entities.LastOrDefault();
if (lastEntity != null)
item.Id = lastEntity.Id + 1;
else
item.Id = 1;
XmlEntities.Add(item);
_xmlContext.SaveChanges(XmlEntities);
}
return item;
}
public T Update(T item)
{
_xmlContext.SaveChanges((IList<T>)Entities);
return item;
}
}
Again really simple implementation and also has another constructor reference (XmlContext
).
We will get to resolution and setup of Unity shortly but given this we now have a two generic data access repositories. One for EntityFramework and one for Xml File Store.
Now back to the EfRepository
it has a constructor parameter DbContext dbContext
this is required as we need access to the underlying data context and connections. Now this is actually an injected class of type EfDataContext
which has an initializer and a few overloads (plus dependency injection).
public class EfDataContext : DbContext
{
public static void Initialize()
{
var connectionFactory = new SqlConnectionFactory();
#pragma warning disable CS0618
Database.DefaultConnectionFactory = connectionFactory;
#pragma warning restore CS0618
}
public EfDataContext(ApplicationSettings applicationSettings)
: base(applicationSettings.DataConnectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new TestEntityMap());
base.OnModelCreating(modelBuilder);
}
}
The class above is really used to override the OnModelCreating
method to add my Fluent configurations for entity mapping (out of scope here) and to Initialize my connection Factory. You will also note the ApplicationSettings
constructor parameter.
Now for the XmlRepository
has its own context that is used to load the xml files and save the entities to xml files. This is a very basic (not thread safe) example.
public partial class XmlContext
{
public XmlContext(ApplicationSettings appSettings)
{
_xmlFileLocation = appSettings.DataConnectionString;
}
readonly string _xmlFileLocation;
public void SaveChanges<T>(IList<T> entities)
where T : BaseEntity
{
if (entities == null)
return;
string filePath = Path.Combine(_xmlFileLocation, $"{typeof(T).Name}.xml");
using (var writer = new StreamWriter(filePath))
{
var serializer = new XmlSerializer(typeof(List<T>));
serializer.Serialize(writer, entities);
writer.Flush();
}
}
public IList<T> Set<T>()
where T : BaseEntity
{
string filePath = Path.Combine(_xmlFileLocation, $"{typeof(T).Name}.xml");
IList<T> entities = new List<T>();
if (File.Exists(filePath))
{
//XDocument doc = XDocument.Load(filePath);
using (var stream = File.OpenRead(filePath))
{
var serializer = new XmlSerializer(typeof(List<T>));
entities = serializer.Deserialize(stream) as List<T>;
}
}
return entities.OrderBy(e => e.Id).ToList();
}
}
This simple class uses the built in XmlSerializer to read\save files to disk by naming the files {Type.Name}.xml. You will also notice it has the ApplicationSettings
constructor parameter.
So far we have setup our Contexts and Repositories for access both connected databases (via EntityFramework) and Xml file storage. The next step is to put it together and register everything in our Unity container. This is where the application settings come in handy.
public class ApplicationSettings
{
[JsonProperty("dataProvider")]
public DataProviderType DataProvider { get; set; }
[JsonProperty("dataConnectionString")]
public string DataConnectionString { get; set; }
}
public enum DataProviderType
{
SqlServer,
XmlFileStore
}
This is a simple Json text file that we can parse and load our settings (however I just did it manually in the example). Your implementation can be done any way. The important thing to note for the DataProvider.SqlServer
the DataConnectionString
is the database connection string and DataProvider.XmlFileStore
is the root location to store the xml files.
static void Main(string[] args)
{
var appSettings = new ApplicationSettings
{
DataConnectionString = @"D:\xmltest\",
DataProvider = DataProviderType.XmlFileStore
};
RegisterContainer(appSettings);
var repo = _container.Resolve<IRepository<TestEntity>>();
var entity = new TestEntity
{
EntityName = "Hello Stack Oveflow"
};
repo.Insert(entity);
}
static UnityContainer _container;
public static void RegisterContainer(ApplicationSettings settings)
{
_container = new UnityContainer();
_container.RegisterInstance(settings);
switch (settings.DataProvider)
{
case DataProviderType.SqlServer:
EfDataContext.Initialize();
_container.RegisterType<DbContext, EfDataContext>();
_container.RegisterType(typeof(IRepository<>), typeof(EfRepository<>));
break;
case DataProviderType.XmlFileStore:
_container.RegisterType<XmlContext>();
_container.RegisterType(typeof(IRepository<>), typeof(XmlRepository<>));
break;
default:
throw new Exception($"The DataProvider Type {settings.DataProvider} is unknown");
}
}
Now the fun begins. Basically at this point we know our data access will be through the use of the generic interface IRepository<T>
. We will use our ApplicationSettings
class to dicate the provider and the connection string. From here we adapt our Unity Registration methods to register one instance of the other.
To walk through the RegisterContainer
method we simply pass in the instance of the ApplicationSettings
. We then register the instance of the ApplicationSettings
(essentially a singleton). Next we then determine which type of provider we are going to load.
If the type is DataProvider.SqlServer
we simply setup the EfDataContext
with the Initialize()
method and then register the DbContext
dependency as EfDataContext
(which requires the ApplicationSettings
dependency already registered. Next we register the IRepository<T>
dependency as EfRepository<T>
. Where EfRepository<T>
has the dependency DbContext
which has been registered as EfDataContext
.
Alternativly for the DataProvider.XmlFileStore
type we register the dependency type XmlContext
and then register the IRepository<T>
dependency as XmlRepository<T>
. Where XmlRepository<T>
has the XmlContext
dependency registered earlier.
Now that we have gotten this far we can answer the question. Finally moving back up to the Main
method we can now call _container.Resolve<IRepository<TestEntity>>()
which regardless of our data storage type gives us the same common interface.
Back to the original question
Currently this model is communicating with the database, but if I
wanted (for whatever reason) to change this model to save data in the
XML file, I would have to completely rewrite all my Repository
classes? Or am I missing something here?
Following the strategy above and leveraging your IoC container all you need to rewrite is your IRepository<T>
implementation to your different storage type. For example you could easily write a JSON serializer similar to the XmlRepository<T>
and XmlContext
. You could change your EfRepository<T>
to directly call sql queries.