Repository
A generic implementation of the repository pattern in C#.
Install / Use
/learn @matthewschrager/RepositoryREADME
Repository
Repository is a generic implementation of the Repository pattern in C#. It provides a repository base class Repository<T> that exposes functions to store/retrieve data,
and an object context base class ObjectContext<T> that enables manipulation of data once it's retrieved. It also exposes an Items property that returns
an IQueryable<T> which can be used to perform LINQ queries on the repository.
Implementations
This (code) repository comes with a few Repository implementations. The first and simplest is InMemoryRepository, which acts as a temporary in-memory store useful mainly for testing.
The second is an Entity Framework repository named EFRepository, which uses Entity Framework as its
storage interface. The thid is AzureRepository, an implementation of Repository for Azure Blob Storage.
And the fourth is FileSystemRepository, an implementation that serializes objects directly to the filesystem.
Pull requests for additional implementations (and improvements to existing ones) are welcome and encouraged.
Change Tracking
As you'll see in the examples below, changes to objects accessed via a Repository instance are tracked automatically and committed back to the underlying data store whenever
a call to SaveChanges is made. This functionality is enabled automatically for any class which derives from Repository; if you're writing a new implementation, you'll get
change tracking for free. See the 'Modify an object' example below to get an idea for how this works.
Examples
Store an object:
var repository = new MyConcreteRepository<int>();
repository.Insert(1); // Store a single value
repository.SaveChanges();
repository.Insert(new[] { 2, 3, 4, 5 }); // Store a bunch of values
repository.SaveChanges();
Retrieve an object:
class MyClass
{
public String Key { get; set; }
}
/* ... */
using (var repository = new MyConcreteRepository<MyClass>())
{
var objectContext = repository.Find("myKey");
var obj = objectContext.Object;
// Do cool things with this value
}
Query stored objects:
class MyClass
{
public String Key { get; set; }
public int Value { get; set; }
}
/* ... */
using (var repository = new MyConcreteRepository<MyClass>())
{
// Query objects based on their Value field
var filteredItems = repository.Items.Where(x => x.Value > 5);
// Do stuff with query result
}
Remove an object:
class MyClass
{
public String Key { get; set; }
}
/* ... */
using (var repository = new MyConcreteRepository<MyClass>())
{
repository.RemoveByKey("myKey");
repository.SaveChanges();
// Or...
var obj = new MyClass { Key = "myKey" };
repository.Remove(obj);
repository.SaveChanges();
// Or...
repository.RemoveByKey("myKey");
repository.SaveChanges();
// Or...
repository.RemoveAllByKey("myKey");
repository.SaveChanges();
// Or...
repository.RemoveAll();
repository.SaveChanges();
}
Modify a stored object:
class MyClass
{
public String Key { get; set; }
public int Value { get; set; }
}
/* ... */
// Pull the object from the repository
using (var repository = new MyConcreteRepository<MyClass>())
{
// Pull the object from the repository
var valueContext = repository.Find("myKey");
// Modify it
valueContext.Object.Value = 5;
// Save the changes
repository.SaveChanges();
}
Typed Key Repositories
Stored objects are accessed by their keys, as in the examples above. Normally, keys are passed into Repository methods as untyped params arguments; as a result,
the compiler will not perform any checks for either the number or types of keys passed in. This allows for flexibility in the types of keys you can use,
but also negates some of the benefits of having a strongly-typed generic repository in the first place.
In order to fix this potential problem, this library contains a series of Repository base classes that take type parameters for keys. These classes
simply wrap the "untyped" repositories with strongly-typed versions of certain methods, like Find and RemoveByKey. In order to make typed versions of
your repositories available, simply derive from the strongly-typed versions of Repository and pass in instances of your untyped repositories to the base
constructor.
For example, EFRepository exposes strongly-typed versions (for classes with up to two key values) like so:
public class EFRepository<TContext, TValue, TKey> : Repository<TValue, TKey> where TValue : class where TContext : DbContext
{
//===============================================================
public EFRepository(Func<TContext, DbSet<TValue>> setSelector, TContext context = null)
: base(new EFRepository<TContext, TValue>(setSelector, context))
{}
//===============================================================
}
public class EFRepository<TContext, TValue, TKey1, TKey2> : Repository<TValue, TKey1, TKey2>
where TValue : class
where TContext : DbContext
{
//===============================================================
public EFRepository(Func<TContext, DbSet<TValue>> setSelector, TContext context = null)
: base(new EFRepository<TContext, TValue>(setSelector, context))
{ }
//===============================================================
}
All these classes do is instantiate untyped instances of EFRepository and pass them into the base Repository constructor. Once instantiated,
these strongly-typed versions of EFRepository will enforce type safety on all key lookups. You can see this by looking at methods that make use of keys. For
example, the signature of Find in the strongly-typed version of Repository looks like this:
public ObjectContext<TValue> Find(TKey key)
{
return InnerRepository.Find(key);
}
Notice how, instead of params Object[], the sole argument here is of type TKey. This will allow the compiler to check for invalid lookups at compile-time,
which can be very handy.
EFRepository
In order to create an instance of EFRepository, you need to first declare a derived class of
DbContext
with DbSet instances for all of the types for which you want to have repositories. An example DbContext might look like this:
class TestClass
{
[Key]
public String Key { get; set; }
}
/* ... */
class MyContext : DbContext
{
public DbSet<TestClass> TestClasses { get; set; }
}
Once a subclass of DbContext is defined, you can instantiate an EFRepository for type TestClass like so:
var repository = new EFRepository<MyContext, TestClass>(x => x.TestClasses);
The sole argument to the constructor is a Func<MyContext, TestClass> that tells the repository how to find the appropriate DbSet in the DbContext.
Note that the procedures normally followed when using Entity Framework must still be followed here; that is, you should still specify connection strings in
your Web.config and decorate your key properties with [Key] attributes if necessary.
AzureRepository
All you need for AzureRepository is a proper connection string for Azure blob storage.
You can either pass this connection string directly to an instance of AzureRepository, or you can store it in a App.config/Web.config file and reference it by name.
There's also a static ForStorageEmulator method that will create an instance of AzureRepository pointed at the local
Azure storage emulator.
Re-using the TestClass class from above, instantiating an AzureRepository looks like this:
// From explicit connection string
var repository = AzureRepository<TestClass>.FromExplicitConnectionString(x => x.Key, myConnectionString);
// From named connection string
var repository = AzureRepository<TestClass>.FromNamedConnectionString(x => x.Key, "myConnectionStringName");
// For storage emulator
var repository = AzureRepository<TestClass>.ForStorageEmulator(x => x.Key);
There is also an AzureOptions class that allows you to specify how e.g. objects are serialized to blob storage, access rules for stored objects, content type, etc.
FileSystemRepository
FileSystemRepository is an implementation that (surprise) saves objects to the local filesystem. Instantiating one is very simple:
var repository = new FileSystemRepository<TestClass>(x => x.Key);
You can also optionally specify a FileSystemOptions parameter that configures how objects are serialized to disk, where they're stored, the file extension, etc.
Implementing Your Own Repositories
To understand how to go about doing implementing your own repositories, we need to briefly go over how repositories work under the hood.
All operations on Repository implementations are deferred; that is, they are not committed until a call to
SaveChanges is made. This is achieved by storing a list of pending operations and applying them
sequentially when SaveChanges is called. These pending operations are objects which implement the
interface Operation, which has a single method called Apply:
public interface Operation
{
//===============================================================
void Apply();
//==========================================
