In this recipe, we will start persisting the Todo
objects added to TodoNancy
through POST
requests to a data store. We will also persist edits done through PUT
requests and remove Todos
from the data store, which are removed through DELETE
requests. The implementation of the data store for the Todo
objects is not very interesting. It can easily be done against any database and is left as an exercise for you. For inspiration, you can have a look at the code download for this recipe where the data store is implemented against MongoDB.
This recipe will show you how Nancy uses dependency injection to make taking dependencies in modules easy and how to use a Nancy bootstrapper
to configure these dependencies if necessary.
This recipe builds on the previous recipe and assumes that you have the TodoNancy
and TodoNancyTests
projects all set up.
The following steps will show you how to manage the dependencies your modules may have:
First, we add a new file to the
NancyTodoTests
project, call itDataStoreTests
, and add the tests below to it. These tests use theFakeItEasy
mock library (https://github.com/FakeItEasy/FakeItEasy), which is a straightforward and flexible mocking framework, that I find complements the style ofNancy.Testing
well.public class DataStoreTests { private readonly IDataStore fakeDataStore; private Browser sut; private readonly Todo aTodo; public DataStoreTests() { fakeDataStore = A.Fake<IDataStore>(); sut = new Browser( with => { with.Dependency(fakeDataStore); with.Module<TodosModule>(); }); aTodo = new Todo() {id = 5, title = "task 10", order = 100, completed = true }; } [Fact] public void Should_store_posted_todos_in_datastore() { sut.Post("/todos/", with => with.JsonBody(aTodo)); AssertCalledTryAddOnDataStoreWtih(aTodo); } private void AssertCalledTryAddOnDataStoreWtih(Todo expected) { A.CallTo(() => fakeDataStore.TryAdd(A<Todo> .That.Matches(actual => { Assert.Equal(expected.title, actual.title); Assert.Equal(expected.order, actual.order); Assert.Equal(expected.completed, actual.completed); return true; } ))) .MustHaveHappened(); }
This test uses a HTTP
POST
method to send a newtodo
object to ourTodoNancy
application and then asserts that a similarTodo
object was added to the fake data store created in the constructor. The way theBrowser
object is created in the constructor is of special interest. Until now, we have createdBrowser
objects based onDefaultNancyBootstrapper
. From now on, we will need to take a bit more control over how theBrowser
object is set up. In particular, we will set up the fake data store object,fakeDataStore
, as a dependency and we tell Nancy to only look for one module, namelyTodosModule
.To satisfy this test, we will first add an
IDataStore
interface to theTodoNancy
project as shown here:public interface IDataStore { IEnumerable<Todo> GetAll(); long Count { get; } bool TryAdd(Todo todo); bool TryRmove(int id); bool TryUpdate(Todo todo); }
Then, we need to change the
TodosModule
class so that it takes a dependency onIDataStore
:public TodosModule(IDataStore todoStore) : base("todos")
And lastly, we need to change the handler for
POST
inTodosModule
to usetodoStores
as follows:Post["/"] = _ => { var newTodo = this.Bind<Todo>(); if (newTodo.id == 0) newTodo.id = todoStore.Count + 1; if (!todoStore.TryAdd(newTodo)) return HttpStatusCode.NotAcceptable; return Response.AsJson(newTodo).WithStatusCode(HttpStatusCode.Created); };
We then move on to test the use of
IDataStore
by the other handlers. The implementation details of these are left to you, the reader, or can be found in code download for this recipe. Such a test will drive changes to theTodosModule
class so that it becomes as follows:public class TodosModule : NancyModule { public static Dictionary<long, Todo> store = new Dictionary<long, Todo>(); public TodosModule(IDataStore todoStore) : base("todos") { Get["/"] = _ => Response.AsJson(todoStore.GetAll()); Post["/"] = _ => { var newTodo = this.Bind<Todo>(); if (newTodo.id == 0) newTodo.id = todoStore.Count + 1; if (!todoStore.TryAdd(newTodo)) return HttpStatusCode.NotAcceptable; return Response.AsJson(newTodo) .WithStatusCode(HttpStatusCode.Created); }; Put["/{id}"] = p => { var updatedTodo = this.Bind<Todo>(); if (!todoStore.TryUpdate(updatedTodo)) return HttpStatusCode.NotFound; return Response.AsJson(updatedTodo); }; Delete["/{id}"] = p => { if (!todoStore.TryRmove(p.id)) return HttpStatusCode.NotFound; return HttpStatusCode.OK; }; } }
At this point, our
NancyTodos
application is ported over to use theIDataStore
interface to store thetodo
object, but there is still no implementation of theIDataStore
interface; so, if you try to run the application or any of the other tests, you get a very long exception message at the bottom of which you find this:Nancy.TinyIoc.TinyIoCResolutionException Unable to resolve type: TodoNancy.IDataStore at Nancy.TinyIoc.TinyIoCContainer.ResolveInternal(TypeRegistration registration, NamedParameterOverloads parameters, ResolveOptions options) at Nancy.TinyIoc.TinyIoCContainer.ConstructType(Type requestedType, Type implementationType, ConstructorInfo constructor, NamedParameterOverloads parameters, ResolveOptions options)
This tells us that Nancy is—naturally—not able to find an implementation of
IDataStore
. This in turn means Nancy will not start up the application because it needs anIDataStore
implementation for theTodosModule
class.Any implementation of
IDataStore
will do and I will leave this to you. In the code for download, you can find an implementation that works against the MongoDB (http://www.mongodb.org/) document database. There is a problem though. TheIDataStore
MongoDB implementation needs a connection string in order to connect to a MongoDB server. Nancy cannot know this connection string, so our application code needs to provide the connection string to the MongoDB data store. This kind of setup in Nancy applications belongs in a bootstrapper. In our case, we extend the default Nancy bootstrapper in order to preserve most of the defaults and just a little bit extra as follows:public class Bootstrapper : DefaultNancyBootstrapper { protected override void ConfigureApplicationContainer(TinyIoCContainer container) { base.ConfigureApplicationContainer(container); var mongoDataStore = new MongoDataStore("mongodb://localhost:27010/todos"); container.Register<IDataStore>(mongoDataStore); }
The
TodosModuleTests
function needs its setup code tweaked a little bit to become as follows:public TodosModuleTests() { var database = MongoDatabase.Create("mongodb://localhost:27017/todos"); database.Drop(); sut = new Browser(new Bootstrapper()); aTodo = new Todo { title = "task 1", order = 0, completed = false }; anEditedTodo = new Todo() { id = 42, title = "edited name", order = 0, completed = false }; }
Now all the tests should pass again and the application should start up without errors.
In the preceding section, we introduced a dependency in the TodosModule
class. As discussed in the Building and running your first Nancy application (Simple) recipe, Nancy will instantiate all modules during the application start up and will do so again when a request for a module comes in. In order to instantiate TodosModule
, Nancy will need an instance of IDataStore
. This pattern is called Dependency Injection and is well documented elsewhere (for instance, on Martin Fowlers bliki http://www.martinfowler.com/articles/injection.html). Nancy uses a container to resolve dependencies. The container that Nancy uses by default is TinyIoC
, and we saw how to set up a dependency with TinyIoC
in the bootstrapper
code. The TinyIoC
container by default supports autowiring by scanning assemblies. This means that without any setup, TinyIoC
will be able to resolve dependencies in cases where there is only one possible implementation for the dependency.
The TinyIoC
container can be swapped out further through the bootstrapper
code. There are a number of NuGet packages with the necessary bootstrapper
code to use other containers; for instance, Castle Windsor, StructureMap, Autofac, Unity, and Ninject.
The bootstrapper allows for much more than just setting up the container. For instance, in the Adding static content (Intermediate) recipe, we will use the bootstrapper to set up conventions for locating static files, and in the Handling cross-cutting concerns – Before, After, and Error Hooks (Intermediate) recipe, we will use the bootstrapper to set up a pipeline in which we have the custom code running before and after each request handler is executed.
In fact, the bootstrapper allows you to customize just about everything in your Nancy application, including Nancy's internals. We have only scratched the surface in this recipe.