Introduction
The article presents an open source .NET abstraction layer above NoSQL databases, providing and Entity-Framework-like Repository Pattern above the various database.
Background
While NoSQL is becoming more and more common, an abstraction above the specific database's API, allow separation between the vendor-specific logic and the database access and management logic is amiss.
Using the code
You can download the entire solution including source code and tests from the CodePlex project site or consume the binaries using NuGet (see links in the CodePlex project site or search for PubComp.NoSql).
The API
Basic usage
For those of you familiar with ORMs like Entity Framework, code like this will look familiar:
using (var context = new MyContext(ParametersForTests.ConnectionInfo))
{
var aaaa = new MyEntity
{
Id = Guid.NewGuid(),
Name = "aaaa",
};
var bbbb = new MyEntity
{
Id = Guid.NewGuid(),
Name = "bbbb",
};
context.MyEntities.Add(aaaa);
context.MyEntities.Add(bbbb);
}
And queries such of this won't know you off your feet either:
using (var context = new MyContext(ParametersForTests.ConnectionInfo))
{
var aaaa = context.MyEntities.AsQueryable().Where(ent => ent.Name == "aaaa")
.FirstOrDefault();
var bbbb = context.MyEntities.AsQueryable().Where(ent => ent.Name == "bbbb")
.FirstOrDefault();
}
Defining the context
Defining an interface for a context such as this may not be what you are used to, but should seem reasonable to you:
public interface IMyContext : IDomainContext
{
IEntitySet<Guid, MyEntity> MyEntities { get; }
IEntitySet<String, UserLogin> UserLogins { get; }
new IFileSet<Guid> Files { get; }
}
The definition of the actual context may seem a bit lighter than you are used to:
public class MyContext: MongoDbContext, IMyContext
{
public MyContext(MongoDbConnectionInfo connectionInfo)
: base(connectionInfo)
{
}
public IEntitySet<Guid, MyEntity>MyEntities
{
get;
private set;
}
public IEntitySet<String, UserLogin> UserLogins
{
get;
private set;
}
public new IFileSet<Guid> Files
{
get;
private set;
}
}
And the fact that you do not have to write code or XML or even select tables and columns in a diagram in order to explain your ORM how these entities are mapped to tables might relieve you. Since NoSQL serializes your objects, as is, into the database, requiring no schema, the model is all you really need to modify.
Since collections can be stored under their parent entities, as either a list of objects or a list of IDs, there is no need for an equivalent for many to many tables, therefore, for common usage, a single field is enough for an ID property. ID types that have been tested for basic support include Guid, String and int.
What about indexes?
You can always define indexes within your context class, as so:
private static IndexDefinition MyEntityByName
{
get
{
return new IndexDefinition(
typeof(MyEntity),
new[] { new KeyProperty("Name", Direction.Ascending) },
asUnique: false,
asSparse: true);
}
}
The reason this are defined on the context and not on the entities, is because the context can hold any inheriting entity types and the indexes may be on properties that do not exist on the base type and because I did not want to define indexes on the base type, that refer to property that may only exist on inheriting types.
Swapping between NoSQL vendors
The only change you need to make to move from MongoDB to Redis and back is to change the base class and connection info types of your context like so:
public class MyContext: MongoDbContext, IMyContext
{
public MyContext(MongoDbConnectionInfo connectionInfo)
: base(connectionInfo)
{
}
vs.
public class MyContext: RedisDbContext, IMyContext
{
public MyContext(RedisDbConnectionInfo connectionInfo)
: base(connectionInfo)
{
}
The base class handles all of the database-specific integration code.
NoSQL data modeling
When you design a NoSQL model, you have two types of entities:
- First class entities - entities that have their own collection (equivalent of table)
- Second class entities - entities that are composed under under entities and do not have their own collection.
The first class entities are similar to the entities you are used to mapping to relational databases, however, their properties can be either simple fields, or complex properties, e.g., another object, a collection or a collection of objects. Entities can therefore be stored with either IDs of other entities or the actual objects. You can use these rule of thumb for determining which modeling to use.
- Can the instances of sub-entities stand own (first class) or do they only have meaning under the parent (second class)?
- Do the sub-entities have the same access rules as the parent class? (Security considerations.) If not they should be first class.
- When you load the parent, do you always want to get the sub-entities (second class), or do you want to be able to load them without loading the children (first class)?
When you model sub-entities as first class entities, you end up hold IDs of one entity within another (like foreign keys do in relational database), which will probably lead you to looking for a solution for navigation properties.
Navigation properties
You can define a navigation property like so:
public class EntityWithNavigation : IEntity<Guid>
{
public Guid Id { get; set; }
public String Name { get; set; }
public Guid InfoId { get; set; }
[Navigation("InfoId", typeof(InfoBase))]
public Info Info { get; set; }
public IEnumerable<Guid> TagIds { get; set; }
[Navigation("TagIds", typeof(Tag))]
public IEnumerable<Tag> Tags { get; set; }
}
where InfoBase is the base class of Info and the type used to define the collection in which Info is stored.
And use it like this:
uow.EntitiesWithNavigation.SaveNavigation(new[] { item }, new[] { "Info", "Tags" });
and
uow.EntitiesWithNavigation.LoadNavigation(new[] { item }, new[] { "Info", "Tags" });
Where the properties you choose to save or load can be defined on all of the entities or only on some of them (e.g. only on entities of a specific inheriting type).
Other interesting features
Additional attributes in use include DbIgnoreAttribute
, which enables you to prevent storing and retrieving specific properties.
The MongoDb version as supports GridFS - MongoDB's file storage.
Points of Interest
While this abstraction has been implemented above both MongoDB (using MongoDB's official C# driver) and Redis (using ServiceStack.Redis), the abilities of these two database and their .NET clients differ and therefore the abstraction above each one has different limitations. For more details see the readme file.