This article in the WCF by example series introduces RavenDB for persistence purposes.
| | |
Chapter XIV | | Chapter XVI |
The Series
WCF by example is a series of articles that describe how to design and develop a WPF client using WCF for communication and NHibernate for persistence purposes. The series introduction describes the scope of the articles and discusses the architect solution at a high level. The source code for the series is found at CodePlex. With this article, the series introduces RavenDB for persistence purposes.
Chapter Overview
For those new to the series, the persistence components of the eDirectory solution are designed around the unit of work and repository patterns. The TransManager
is responsible for the unit of work implementation, therefore it provides access to the IRepositoryLocator
, which gives access to individual entity repositories. The repositories are based on generic implementations that are created as they are needed by the repository locator. These generic repositories expose basic CRUD methods and endpoint to a IQueryble
method leveraged by the persistence Linq providers
The following is an example of the code used in the server components:
public CustomerDto UpdateCustomer(CustomerDto dto)
{
01 return ExecuteCommand(locator => UpdateCustomerCommand(locator, dto));
}
private CustomerDto UpdateCustomerCommand(IRepositoryLocator locator, CustomerDto dto)
{
02 var instance = locator.GetById<customer>(dto.Id);
03 instance.Update(locator, dto);
return Customer_to_Dto(instance);
}
Line 01: Entry point for the customer service to update an existing instance
Line 02: Using the Dto.Id
property, we use the Locator
to resolve the customer instance
Line 03: Then we delegate to the domain class to update itself passing the locator and the Dto instances
Up to this chapter, we have seen so far two implementations of the persistence components: InMemory
and NHibernate
. In this chapter, we introduce a new implementation: RavenDB.
RavenDB is a document-orientated database that facilitates the persistence of objects. In NHibernate, a mapping is required between the entity classes and the database tables. But in RavenDB, there is not such requirement for additional mapping between entities and the persistence framework.
The article discusses how easy it is to implement a customized set of persistence components so the eDirectory application can save and read objects from the RavenDB document store. We use the Embedded document store implementation in this example and we will also discuss how to set it up to execute tests in the InMemory mode.
As a some sort of disclosure, I decided not to change the domain entities that we have been used so far during the series, as a result, you might find that the best design practices for a RavenDB solution are not strictly followed within this example. Nevertheless, the article should be a good example of how to get RavenDB working.
In a different line, as with NHibernate, the RavenDB implementation in the eDirectory solution is leveraged by using the Linq provider and generic repositories; in a real scenario, this approach may result too restrictive and customized repository implementations might be required in more complex scenarios.
Besides the mentioned constraints, the proposed architecture in this article could work well in many cases or at least be used as a start point for your projects.
Before
A new project was added to the eDirectory
solution named eDirectory.RavenDB
. Then NuGet was used to get a reference to RavenDB Embedded. It is worth noting that a post-build event was added so all the build artifacts are copied to the libs folder facilitating the usage in the client project.
RavenDB Repository
The repository implementation is very similar to the NHibernate in many aspects:
public class RepositoryRavenDB<TEntity>
: IRepository<TEntity>
{
private readonly IDocumentSession _sessionInstance;
private readonly string _idPrefix;
public RepositoryRavenDB(IDocumentSession session)
{
_sessionInstance = session;
01 _idPrefix = GetPrefix();
}
private static string GetPrefix()
{
var typeName = typeof (TEntity).Name;
var flag = typeName.Last().Equals('s');
02 return typeName +
(flag
? "es/"
: "s/");
}
#region Implementation of IRepository<TEntity>
public TEntity Save(TEntity instance)
{
03 _sessionInstance.Store(instance);
return instance;
}
public void Update(TEntity instance)
{
}
public void Remove(TEntity instance)
{
04 _sessionInstance.Delete(instance);
}
public TEntity GetById(long id)
{
05 return _sessionInstance.Load<TEntity>(_idPrefix + id);
}
public IQueryable<TEntity> FindAll()
{
06 return _sessionInstance.Query<TEntity>();
}
#endregion
}
Line 01: For each repository/entity type, a prefix is generated to be used by the Load
method.
Line 02: Prefixes are terminated in plural, for entity names that finish with an 'S
', we need an additional check.
Line 03: When the Store
method is invoked, a new instance is saved in RavenDB, there is no need for additional mapping to get this working, which is a cool feature. Also, RavenDB detects the Id property in our entities by name convention and the property is populated once the method is returned.
Line 04: The Delete
method is used to remove entities from the database.
Line 05: The Load
method is the optimized mechanism to retrieve entities by Id, this is where the prefix needs to be used.
Line 06: The FindAll
method delegates to the RavenDB Linq provider, exactly the same way it is being done in the NHibernate implementation.
Also another aspect to notice, the Update
method is not required in the RavenDB implementation.
Repository Locator
The locator implementation is very simple, it is very much the same we have seen before:
public class RepositoryLocatorRavenDB
: RepositoryLocatorBase, IResetable, IStoreInitialiser
{
private readonly IDocumentSession _sessionInstance;
public RepositoryLocatorRavenDB(IDocumentSession session)
{
_sessionInstance = session;
}
#region Overrides of RepositoryLocatorBase
protected override IRepository<T> CreateRepository<T>()
{
01 return new RepositoryRavenDB<T>(_sessionInstance);
}
#endregion
#region Implementation of IResetable
02 ...
#endregion
#region Implementation of IStoreInitialiser
02 ...
#endregion
}
Line 01: The base locator class calls this method to obtain an instance of a repository for a given type.
Line 02: These two sections are omitted for the time being as their purpose is solely for testing reasons, we will discuss them later within this article.
Transaction Manager
Again, the implementation of this class is very similar to the NHibernate one:
public class TransManagerRavenDB
: TransManagerBase
{
private readonly IDocumentSession _sessionInstance;
public TransManagerRavenDB(IDocumentSession session)
{
_sessionInstance = session;
01 Locator = new RepositoryLocatorRavenDB(_sessionInstance);
}
#region Overriden Base Methods
public override void CommitTransaction()
{
base.CommitTransaction();
02 _sessionInstance.SaveChanges();
}
public override void Rollback()
{
base.Rollback();
03 _sessionInstance.Advanced.Clear();
}
protected override void Dispose(bool disposing)
{
...
}
private void Close()
{
...
}
#endregion
}
Line 01: A IDocumentSession
is passed in the constructor that is used for the creation of the RepositoryLocatorRavenDB
.
Line 02: To commit all the changes done since the transaction manager was created, we need to invoke the SaveChanges
method in the IDocumentSession
.
Line 03: The Clear
is used to discard all the changes if a rollback is required.
It is worth nothing that there is no specific mechanism for starting the transaction like we have in NHibernate.
Transaction Manager Factory
This component is responsible for two key roles; the creation of the IDocumentSession
and the TransManager
:
public class TransManagerFactoryRavenDB
: ITransFactory
{
private IDocumentStore _documentStore;
private IDocumentStore DocumentStore
{
get
{
03 if (_documentStore != null) return _documentStore;
_documentStore = InitialiseDocumentStore();
return _documentStore;
}
}
private IDocumentStore InitialiseDocumentStore()
{
var store = new EmbeddableDocumentStore { DataDirectory = "eDirectory" };
01 store.Initialize();
return store;
}
#region Implementation of ITransFactory
public ITransManager CreateManager()
{
02 return new TransManagerRavenDB(DocumentStore.OpenSession());
}
#endregion
}
Line 01: Creates an embeddable document store instance named "eDirectory
", this would automatically generate a RavenDB instance on your deployment folder. The Initialize
method is critical before any action can be invoked against the store.
Line 02: This is the factory method that returns a Transaction Manager passing a new session to its constructor. Like with NHibernate, this session instance is not available to the code besides the Locator and Repositories. This is a key aspect to get the unit of work correctly working.
Line 03: You may want to enhance this method to avoid a race condition, for example, a lazy initialization of the document store would be advisable.
How to Configure the Client UI
In order to get the WPF Client to use the RavenDB, just follow next instructions:
- Ensure all projects build correctly, you may want to manually delete all the files in your Debug or Release folder
- Modify the App.config in the client (
eDirectory.WPF
) so the SpringConfigFile
is set to: file://RavenDBConfiguration.xml - Execute the client project
- Create a new customer using the 'Customer with address view'
At this point, after creating the first customer instance, you will probably get an empty grid like the following:
We create the first customer:
And after pressing the Save button, the customer grid is empty:
If we wait 10-15 secs and we refresh pressing the command button, the customer record appears:
What we have replicated here is what is called a stale index state in RavenDB, it seems that when the first record is created in a RavenDB store instance for the first time, the index takes a long time to be updated and the query does not return the record that was just created. It is worth noting that the refresh is done in a different request than the one for the save action, in this case.
If you close the application and restart it once more, you see this issue does not happen again, even if an address instance is created for the first time. You may want to have a look at the official documentation regarding this 'feature'. We will discuss it again in the Test section and see how we can avoid this sort of situation.
Other Aspects
There is one additional change required in the Domain entities, we need to tag the customer and address entities with the JsonObject
attribute so the NewtonSoft
library can serialize our objects correctly. There is also a change in our Linq queries where the 'Equals
' must be replaced by '==
'; the RavenDB Linq struggles if the 'Equals
' method is used.
Testing with RavenDB
If we want to use RavenDB when running our tests, there is a key aspect that we need to consider:
- Tear down the database between test executions
InMemory
But before we discuss how to tear down the document store, there is an interesting feature worth mentioning: RavenDb can be configured to run in-memory, hence performance is improved without losing any behavior/functionality. As a result, I modified the TransManagerFactoryRavenDB
so the tests can be run in memory:
public class TransManagerFactoryRavenDB
: ITransFactory
{
...
01 private bool IsSetForTesting { get; set; }
private IDocumentStore InitialiseDocumentStore()
{
var store =
IsSetForTesting
02 ? new EmbeddableDocumentStore
{
RunInMemory = true
}
: new EmbeddableDocumentStore {DataDirectory = "eDirectory"};
store.Initialize();
return store;
}
...
}
Line 01: A new flag can be set to indicate that an InMemory
instance must be created.
Line 02: If the flag is set, then the RavenDB
instance is created in memory.
The Spring configuration file used for the tests sets the IsSetForTesting
property so when the tests are executed an InMemory RavenDB
instance is used instead. You may want to look at the TestRavenDBConfiguration
file located at the test project for more details.
Tear Down
I took the idea of using an index from Vladimir Petrov's blog, so when entity instances are created during the tests, they can be deleted using a customized index. RavenDB
indexes are created in code by inheriting from the AbstractIndexCreationTask
class:
public class AllDocumentsById : AbstractIndexCreationTask
{
public const string Name = "AllDocumentsById";
#region Overrides of AbstractIndexCreationTask
public override IndexDefinition CreateIndexDefinition()
{
return new IndexDefinition
{
01 Name = AllDocumentsById.Name,
02 Map = "from doc in docs let DocId =
doc[\"@metadata\"][\"@id\"] select new {DocId};"
};
}
#endregion
}
Line 01: We give the index a well known name, we'll need it when we have to delete the entities.
Line 02: Declares the index mapping so it creates an entry for any doc created in the store.
In order to create the index, the test project has some extra functionality to determine if the RepositoryLocator
implements the IStoreInitialiser
interface, if so, it invokes the ConfigureStore
method. The RepositoryLocatorRavenDB
does so:
public class RepositoryLocatorRavenDB
01 : RepositoryLocatorBase, IResetable, IStoreInitialiser
{
...
#region Implementation of IStoreInitialiser
public void ConfigureStore()
{
var documentStore = _sessionInstance.Advanced.DocumentStore
as EmbeddableDocumentStore;
if (documentStore == null) return;
02 IndexCreation.CreateIndexes(typeof(AllDocumentsById).Assembly, documentStore);
}
#endregion
}
So how do the tests use the index?. Well, it happens that the RepositoryLocatorRavenDB
also implements the IResetable
interface:
public class RepositoryLocatorRavenDB
: RepositoryLocatorBase, IResetable, IStoreInitialiser
{
...
#region Implementation of IResetable
public void Reset()
{
var documentStore = _sessionInstance.Advanced.DocumentStore
as EmbeddableDocumentStore;
if (documentStore == null) return;
01 while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0)
{
Thread.Sleep(10);
}
02 _sessionInstance.Advanced.DatabaseCommands.DeleteByIndex
(AllDocumentsById.Name, new IndexQuery());
}
#endregion
...
}
Line 01: I will explain in a second.
Line 02: It uses the DeleteByIndex
passing the name of our customized index.
There is a problem with the above index: when the test execution only creates one entity instance, the AllDocumentsById
index is stale. If more than one instance is created, it seems that the issue is resolved. As a result, line 01 is used to ensure that the index is not stale when the deletion method is invoked.
Set Configuration to Use RavenDB
In order to use the RavenDB implementation when running the tests, ensure that the App.config is configured so the SpringConfigFile
appSetting is set to TestRavenDBConfiguration.xml:
Conclusion
It has been very easy to adapt RavenDB to the eDirectory solution, in fact, it just took a couple hours to get it working, getting the tests working took a little more time as a result of the stale index issue. Not having to create a mapping between the domain entities and the document store makes the difference.
As a note: if best design practices are to be followed when designing the domain for a RavenDB store, you need to move away from the traditional relational approach, documents should store as much information as possible; what does it mean? in a nutshell, in domain terms, your documents become aggregates and duplication is not a 'sin'. It is a radical change in the way of thinking if you have been using something like NHibernate or any other ORM framework.
Other Resource Links
History
- 4th February, 2013: Initial version