Skip to content

stepaside/Yarn

Repository files navigation

Yarn

yet another repository for .net

Yarn is a generic repository pattern implementation for .Net. Its goal is to enforce consistent approach to data persistence regardless of the underlying technology.

Here is what it currently supports:

  • EF
  • EF Core
  • NHibernate
    • SQL Server
    • MySql
    • SQLite
    • Oracle
    • Postgres
  • RavenDB
  • MongoDB
  • In-memory object storage
  • Nemo (micro-ORM library)
  • EventStore

Quick example of the pattern usage

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository>(() => new Yarn.Data.MongoDbProvider.Repository(
                                                      new RepositoryOptions 
                                                      { 
                                                         ConnectionString = ConfigurationManager.AppSettings["MongoConnection"] 
                                                      }));

// Resolve IRepository (this may happen anywhere within application)
// Let's assume we have defined entity Category
var repo = ObjectContainer.Current.Resolve<IRepository>();
var category = repo.GetById<Category, int>(1000);
var categories = repo.GetByIdList<Category, int>(new[] { 1000, 1100 });

IoC with Yarn

// Yarn provides a simple IoC container implementation
// One can easily override it with any DI framework 
// of choice by implementing IContainer
// The following code should be called on application startup
// in order to override IoC container
ObjectContainer.Initialize(() => new Any_IoC_Implementation_Based_On_IContainer());

// IRepository is instantiated using default constructor of Yarn.Data.EntityFrameworkProvider.Repository
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();

// IRepository is instantiated using parametrized constructor of Yarn.Data.EntityFrameworkProvider.Repository
ObjectContainer.Current.Register<IRepository>(() => new Yarn.Data.EntityFrameworkProvider.Repository(
                                                      ObjectContainer.Current.Resolve<IDataContext<DbContext>>()));
  
// IRepository is instantiated unders a specific instance name
ObjectContainer.Current.Register<IRepository>(() => new Yarn.Data.EntityFrameworkProvider.Repository(
                                                      ObjectContainer.Current.Resolve<IDataContext<DbContext>>()), "Lazy");

// IRepository is instantiated as a singleton
ObjectContainer.Current.Register<IRepository>(new Yarn.Data.EntityFrameworkProvider.Repository(
                                                      ObjectContainer.Current.Resolve<IDataContext<DbContext>>()));

// Resolved IRepository implementation
var repo = ObjectContainer.Current.Resolve<IRepository>();
// IRepository is resolved by instance name
var repo = ObjectContainer.Current.Resolve<IRepository>("Lazy");

Slightly more sophisticated example utilizing multiple implementations of IRepository

// Bind IRepository to specific implementation (this should happen during application startup)
// For this example one must provide "EF.Default.Model" application setting and "EF.Default.Connection" connection string setting
// ("EF.Default.Model" points to an assembly which contains model class definition)
ObjectContainer.Current.Register<IRepository>(() => new Yarn.Data.MongoDbProvider.Repository(
                                                      new RepositoryOptions 
                                                      { 
                                                         ConnectionString = ConfigurationManager.AppSettings["MongoConnection"] 
                                                      }), "mongo");
ObjectContainer.Current.Register<IRepository>(() => new Yarn.Data.EntityFrameworkProvider.Repository(
                                                      ObjectContainer.Current.Resolve<IDataContext<DbContext>>()), "ef");

// Resolve IRepository (this may happen anywhere within application)
// "mongo" will resolve to MongoDB implementation, while "ef" will resolve to EF implementation
var repo = ObjectContainer.Current.Resolve<IRepository>("ef");
//var repo = ObjectContainer.Current.Resolve<IRepository>("mongo");
var category = repo.GetById<Category, int>(1000);

NHibernate implementation of IRepository

// With NHibernate one must specify implementation of the data context to be used with repository
// For this example one must provide "NHibernate.MySqlClient.Model" application setting 
// and "NHibernate.MySqlClient.Connection" connection string setting
ObjectContainer.Current.Register<IRepository>(() => new Yarn.Data.NHibernateProvider.Repository(
                                                  ObjectContainer.Current.Resolve<IDataContext<ISession>>("nh_uow_mysql")), "nh");
ObjectContainer.Current.Register<IDataContext<ISession>>(
  () => new Yarn.Data.NHibernateProvider.MySqlClient.MySqlDataContext(), "nh_uow_mysql");

// In order to use NHibernate with SQL Server one has to bind IDataContext to the SQL Server implementation
// Similarly to the MySQL example "NHibernate.SqlClient.Model" application setting 
// and "NHibernate.SqlClient.Connection" connection string setting should be defined
// ObjectContainer.Current.Register<IDataContext<ISession>>(() =>
                                                  new Yarn.Data.NHibernateProvider.SqlClient.Sql2012DataContext(), "nh_uow_sql");

var repo = ObjectContainer.Current.Resolve<IRepository>("nh");
var categories = repo.FindAll<Category>(c => c.Name.Contains("cat"), offset: 50, limit: 10);

Full text search implementation with IRepository

  • EF

    // Currently works only with SQL Server provider for EF
    ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.FullTextRepository>();
    ObjectContainer.Current.Register<IFullTextProvider, Yarn.Data.EntityFrameworkProvider.SqlClient.SqlFullTextProvider>();
    
    var repo = ObjectContainer.Current.Resolve<IRepository>();
    var categories = ((IFullTextRepository)repo).FullText.Seach<Category>("hello world");
  • NHibernate

    ObjectContainer.Current.Register<IRepository, Yarn.Data.NHibernateProvider.FullTextRepository>();
    ObjectContainer.Current.Register<IFullTextProvider, Yarn.Data.NHibernateProvider.LuceneClient.LuceneFullTextProvider>();
    // One can resort to use of SQL Server full text as well
    // ObjectContainer.Current.Register<IFullTextProvider, Yarn.Data.NHibernateProvider.SqlClient.SqlFullTextProvider>();
    
    var repo = ObjectContainer.Current.Resolve<IRepository>();
    var categories = ((IFullTextRepository)repo).FullText.Seach<Category>("hello world");

Specification pattern implementation with IRepository

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();

// Resolve IRepository (this may happen anywhere within application)
var repo = ObjectContainer.Current.Resolve<IRepository>();

// Create a specification to abstract search criteria
var spec = new Specification<Category>(c => c.Name.Contains("hello")).Or(c => c.Name.Contains("world"));
var categories = repo.FindAll<Category>(spec);

Utilizing caching with IRepository

// Implement a cache provider to support caching of your choice
public class SimpleCache : ICacheProvider
{
  // Implementation goes here
}

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();

// Resolve IRepository (this may happen anywhere within application)
var repo = ObjectContainer.Current.Resolve<IRepository>();

// Initialize cache repository adapter
// Note: Cache adapter implements write-through cache for entity-related 
// operations and generational caching for queries
var cachedRepo = repo.WithCache<SimpleCache>();

// Create a specification to abstract search criteria
var spec = new Specification<Category>(c => c.Name.Contains("hello")).Or(c => c.Name.Contains("world"));

// This call produces a cache miss, hence the database is hit
var categories1 = cachedRepo.FindAll<Category>(spec);

// This call produces a cache hit, hence there will be no trip to the database
var categories2 = cachedRepo.FindAll<Category>(spec);

Eager loading

In order for this to work IRepository implementation must also implement ILoadServiceProvider. NoSQL Yarn providers such as Yarn.MongoDB and Yarn.RavenDB do not implement this interface.

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();

// Resolve IRepository (this may happen anywhere within application)
var repo = ObjectContainer.Current.Resolve<IRepository>();

// Load customer with orders and order details
var customer = repo.As<ILoadServiceProvider>()
                  .Load<Customer>()
                  .Include(c => c.Orders)
                  .Include(c => c.Orders.Select(o => o.Order_Details))
                  .Find(c => c.CustomerID == "ALFKI");

Object graph merging

Object graph merging only works for repositories that implement ILoadServiceProvider interface.

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();

// Resolve IRepository (this may happen anywhere within application)
var repo = ObjectContainer.Current.Resolve<IRepository>();

// Merge customer changes with customer data from the database
// Yarn will attempt to merge only the changes specified by the navigation paths
// Note: currently only EF, NHibernate and Nemo providers implement this functionality
var mergedCustomer = repo.As<ILoadServiceProvider>()
                  .Load<Customer>()
                  .Include(c => c.Orders)
                  .Include(c => c.Orders.Select(o => o.Order_Details))
                  .Update(customer);

Repository adapters

// Auditable adapter will automatically populate audit information 
// when calling Add/Update for all entities which implement IAuditable interface
var repo = ObjectContainer.Current.Resolve<IRepository>().WithAudit(Thread.CurrentPrincipal);

// Soft-delete adapter will automatically re-write Remove as Update 
// and will filter all deleted records out on retrieve for all entities
// which implement ISoftDelete interface
var repo = ObjectContainer.Current.Resolve<IRepository>().WithSoftDelete(Thread.CurrentPrincipal);

// Multi-tenancy adapter will automatically filter tenant related data as well 
// as check tenant ownership when calling Add, Update and Remove for all entities 
// which implement ITenant interface
// Note: the following example assumes Thread.CurrentPrincipal implements ITenant interface
var repo = ObjectContainer.Current.Resolve<IRepository>().WithMultiTenancy((ITenant)Thread.CurrentPrincipal);

// Fail-over adapter can use an alternative repository if one becomes unavailable
// Once the fail-over happens, the repo adapter will stick to that one until it becomes unavailable
// Updates however are sent to both repositories
// Note: one can use it with more than two repository implementations by chaining
var repo = ObjectContainer.Current.Resolve<IRepository>().WithFailover(ObjectContainer.Current.Resolve<IRepository>("no-sql"));

// It is also possible to chain the adapters
// Note: IPrincipal parameter is optional for soft-delete and auditable adapters
var repo = ObjectContainer.Current.Resolve<IRepository>().WithSoftDelete().WihAudit();

Bulk operations

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();

// Resolve IRepository (this may happen anywhere within application)
var repo = ObjectContainer.Current.Resolve<IRepository>();

// As of now bulk operations are implemented by EF and Mongo providers only
var bulk = repo.As<IBulkOperationsProvider>();

// Bulk retrieve
var customers = bulk.GetById<Customer, string>(new[] { "ALFKI", "ANTON"  });

// Bulk delete by id
bulk.Delete<Customer, string>(new[] { "ALFKI", "ANTON"  });

// Bulk delete
bulk.Delete<Customer>(c => c.City == "London");

// Bulk update
bulk.Update<Customer>(c => c.City == "New York", c => new Customer { City = c.City + " City" });

Quick Interceptor

public class LogInterceptor : IDisposable
{
  private readonly ILog logger = LogManager.GetLogger("my-log");
  private readonly Stopwatch sw;
  
  public LogIntercetor(InterceptorContext ctx)
  {
    sw = Stopwatch.StartNew();
    logger.Info("Begin " + ctx.Method.Name);
    ctx.Execute();
  }
  
  public void Dispose()
  {
    sw.Stop();
    logger.Info("End " + ctx.Method.Name + ", " + sw.Elapsed.TotalMilliseconds + " ms");  
  }
}

// Interceptor will automatically intercept all methods
var repo = ObjectContainer.Current.Resolve<IRepository>().WithInterceptor(ctx => new LogInterceptor(ctx));

Entity specific repository

// Bind IRepository to specific implementation (this should happen during application startup)
ObjectContainer.Current.Register<IRepository, Yarn.Data.EntityFrameworkProvider.Repository>();
ObjectContainer.Current.Register<ICustomerRepository, CustomerRepository>();

public interface ICustomerRepository : IEntityRepository<Customer, string>
{
    IQueryResult<Order> GetOrders(string id);
}

public class CustomerRepository : ICustomerRepository
{
    private readonly IRepository _repo;

    public CustomerRepository(IRepository repo)
    {
        _repo = repo;
    }

    public IQueryResult<Customer> Find(ISpecification<Customer> criteria)
    {
        var customer = _repo.Find(criteria);
        var items = customer != null ? new[] { _repo.Find(criteria) } : new Customer[] { };
        return new QueryResult<Customer>(items, customer != null ? 1 : 0 );
    }

    public IQueryResult<Customer> GetAll()
    {
        return new QueryResult<Customer>(_repo.FindAll(new Specification<Customer>(c => true)), 
                                          _repo.Count<Customer>());
    }

    public Customer GetById(string id)
    {
        return _repo.GetById<Customer, string>(id);
    }

    public IQueryResult<Order> GetOrders(string id)
    {
        return new QueryResult<Order>(_repo.FindAll<Order>(o => o.CustomerID == id), 
                                      _repo.Count<Order>(o => o.CustomerID == id));
    }

    public void Remove(Customer entity)
    {
        _repo.Remove(entity);
    }

    public Customer Remove(string id)
    {
        return _repo.Remove<Customer, string>(id);
    }

    public bool Save(Customer entity)
    {
        return _repo.As<ILoadServiceProvider>().Load<Customer>().Update(entity) != null;
    }
}