Davenport is the comfiest .NET wrapper for CouchDB. Its goal is to simplify interacting with the CouchDB API by wrapping things like getting, listing, creating, updating, finding, copying and deleting documents.
Project build status | |
Davenport package | |
Davenport.Fsharp package | |
License |
Davenport and Davenport.Fsharp will soon be merged into the same Davenport package, with the C# package wrapping the F# package.
Davenport is published on Nuget. You can install Davenport from the dotnet command line:
dotnet add package davenport
Or use the Visual Studio package manager console:
install-package davenport
And if you're using Paket, add this to your paket.dependencies
file:
nuget davenport
# You probably want the F# wrapper if you're using paket
nuget davenport.fsharp
(Want to use Davenport with F#? See below.)
To use Davenport you'll need to create an instance of the Client
class. Each Client
is tied to a specific database which is more akin to a "table" in SQL terms, as each CouchDB installation can host thousands of databases; in fact, with CouchDB such a case is even encouraged with the database-per-user strategy.
To create a Client
, you can either construct the class itself, or you can use Davenport.Configuration.ConfigureDatabaseAsync
which will take care of creating the database (if it doesn't exist), creating design docs, and creating find indexes all at once. ConfigureDatabaseAsync
will also check that your CouchDB instance is at version 2.0, which is required by the FindBy*Async
, CountBy*Async
and ExistsBy*Async
methods.
// Create a Find index on the "Foo" property of documents in this database.
string[] indexes = { "Foo" };
var designDocs = new DesignDocConfig[]
{
new DesignDocConfig()
{
Name = "list",
Views = new View[]
{
new View()
{
// Name must be URL compatible, e.g. no spaces or invalid URL characters.
Name = "only-bazs-greater-than-10",
// Map and Reduce functions must be valid JavaScript strings.
MapFunction = @"function (doc) {
if (doc.Baz > 10) {
emit(doc._id, doc);
}
}",
// Use the built-in _count reduce function.
ReduceFunction = "_count"
}
}
}
};
Client<DocumentType> client;
try
{
client = await Configuration.ConfigureDatabaseAsync<DocumentType>(Config, indexes, designDocs);
}
catch (DavenportException ex)
{
// Handle exception
}
It should be noted that configuring the design docs and their views is a "dumb" process. Davenport will check to see if the design documents you gave it match the ones that exist on your database exactly. That is to say, if you have a design doc named list
on your database, and it has a view called list-foos
, Davenport will check that the map and reduce functions match the ones you passed it exactly, to the letter. If not, Davenport will overwrite the view's map and reduce functions with the ones you gave it.
Davenport does not delete design documents that it encounters but weren't listed in your configuration. It will simply skip them.
If you don't need to configure your database or design docs from code, then you don't need to use ConfigureDatabaseAsync
at all. You can just create a new Client
which is immediately ready to interact with your database:
var client = new Davenport.Client<DocumentType>("http://localhost:5984", "my_database_name");
CouchDB assigns all documents an _id
and _rev
property on every create or update call. To ensure that Davenport can actually execute requests against your database and documents, all objects sent through the Client
must inherit from the Davenport.CouchDoc
class. The CouchDoc
class implements the Id
and Rev
strings for you, which are then mapped to the _id
and _rev
strings when sent to CouchDB.
Like many .NET packages, Davenport uses Newtonsoft.Json to handle de/serialization to and from JSON. That means your document classes can use the entire cadre of Json.Net attributes and serializers on your properties.
For example, you can use the [JsonProperty("foo")]
attribute on a Foo
property, which forces the property name to foo
when sent to your database, and deserializes it back to Foo
when returned. This is what Davenport uses internally to map Id
and Rev
on the CouchDoc
class to _id
and _rev
in CouchDB.
public class MyClass : Davenport.CouchDoc
{
[JsonProperty("foo")]
public string Foo { get; set; }
// Serializes to { "_id" : "some-id", "_rev" : "some-rev", "foo" : "value" }
}
Davenport is configured to ignore null property values when serializing and deserializing.
Note that the code sample below does not cover all methods, just a fraction of them to show what's possible.
using Davenport;
public class MyDoc : CouchDoc
{
string Foo { get; set; }
}
//...
// Create a client for working with the "my_database" database
var client = new Client<MyDoc>("localhost:5984", "my_database");
// Or, use an optional username and password to connect
var client = new Client<MyDoc>(new Configuration("localhost:5984", "my_database")
{
Username = "username",
Password = "password"
});
// Create the database if it doesn't exist
await client.CreateDatabaseAsync();
// Create a doc
var myDoc = await client.PostAsync(new MyDoc()
{
Foo = "Hello world!"
});
// Get a doc
MyDoc doc;
try
{
doc = await client.GetAsync(docId);
// Or get one by a specific revision
doc = await client.GetAsync(docId, rev);
}
catch (DavenportException e)
{
if (e.Status == 404)
{
// Doc was not found
}
else
{
// Some other error
}
}
// Find docs by the value of their Foo property
var docs = await client.FindByExprAsync(d => d.Foo == "Hello world!");
// Or use a dictionary
var docs = await client.FindBySelectorAsync(new Dictionary<string, FindExpression>()
{
{ "Foo", new FindExpression(ExpressionType.Equal, "Hello world!")}
});
I had previously built Davenport with an F# wrapper for the C# methods, making the package much more functional, idiomatic and easier to use with F#. However, I really wanted to make it possible to store several different object types in the same CouchDB database (as is idiomatic for most CouchDB usage). This resulted in a complete rewrite of the F# package; it no longer wraps any C# methods, nor is constrained by any C#-style thinking like it was previously.
In fact, soon the C# client will be itself rewritten to wrap the F# client.
To install the F# package for Davenport, just add the following to your paket.dependencies
file:
nuget davenport.fsharp
IMPORTANT: The default ICouchConverter
in the F# package uses the Microsoft.FsharpLu.Json.Compact
package internally to serialize things like Options, Union Types and so-on to a friendly JSON format that can then be easily deserialized by the same converter.
To summarize, this means that some values in the CouchDB database may not be in the same format that you might expect if you had serialized them with a plain Newtonsoft.Json.JsonConverter
, but it will look much more like "normal" json.
For example, this F# data:
type MyDoc = {
_id: string
_rev: string
numbers: int option list
}
let doc = {
_id = "someId"
_rev = "someRev"
numbers = [Some 5; None; Some 6]
Will get converted to this JSON when sent to CouchDB:
{
"_id": "someId",
"_rev": "someRev",
"numbers": [5, 6]
}
Whereas if we had used the default Newtonsoft.Json.JsonConverter
it would have come out more like this:
{
"_id": "someId",
"_rev": "someRev",
"numbers": [
{
"Case": "Some",
"Fields": [ 5 ]
},
null,
{
"Case": "Some",
"Fields": [ 6 ]
}
]
}
Where the C# package for Davenport only supports one type of document stored per database (and it must extend the CouchDoc
type), the F# package has no such constraints. It lets you pass in an (optional) map containing type names (which map to custom Id/Rev labels on your records), and then stores those type names with the document.
When you retrieve the document you'll receive a string option * Document
tuple -- where the string option
is the doc's type name if it's found -- which you can use to decide how the document should be deserialized.
type FirstDoc = {
Id: string
Rev: string
Numbers: int option list
}
type SecondDoc = {
DocId: string
DocRev: string
Hello: string
}
let docMapping: FieldMapping =
Map.empty
// Tell Davenport to map the 'first-doc' Id to _id and Rev to _rev
|> Map.add "first-doc" ("Id", "Rev")
// Tell Davenport to map the 'second-doc' DocId to _id and DocRev to _rev
|> Map.add "second-doc" ("DocId", "DocRev")
// Create a client connection
let client =
"localhost:5984"
|> database "my_database" // All requests will be to "my_database"
|> mapFields docMapping // Tell Davenport which types have custom _id/_rev field names.
|> username "username" // Optionally use a username to login
|> password "password" // Optionally use a password to login
|> converter someICouchConverter // Optionally use your own custom converter.
// NOTE: This must map your id and rev fields for you.
type MyDoc =
| First of FirstDoc
| Second of SecondDoc
// The Insertable type tells Davenport what string to store as the `type` prop on the doc.
let insertable doc: Insertable<obj> =
match doc with
| MyDoc.First as d -> Some "first-doc", d :> obj
| MyDoc.Second as d -> Some "second-doc", d :> obj
// Insert multiple documents in one request
let! bulkInsertResult =
[
// CouchDB will fill in the Id and Rev values when the docs are created
{ Id = null; Rev = null; Numbers = [Some 5; Some 10] }
|> MyDoc.First
|> insertable
{ MyId = null; MyRev = null; Hello = "world" }
|> MyDoc.Second
|> insertable
]
|> bulkInsert BulkMode.AllowNewEdits
<| client
// Next, we'll list all of the documents that were just inserted since CouchDB doesn't
// return the full document after insert (only Id and Rev values).
let keys =
bulkInsertResult
|> List.filter (function | BulkResult.Inserted _ -> true | BulkResult.Failed _ -> false)
|> List.map (fun (d: PostPutCopyResponse) -> d.Id)
|> ListOption.Keys
let! listResult =
client
|> listAll WithDocs [keys]
let docs =
listResult
|> List.map (fun d ->
match d.TypeName with
| Some "first-doc" ->
d.To<FirstDoc>()
|> MyDoc.First
|> Some
| Some "second-doc" ->
d.To<SecondDoc>()
|> MyDoc.Second
|> Some
| _ ->
// Unknown doc type
None)
|> List.filter Option.isSome
// You now have a list of MyDoc.First and MyDoc.Second!
For more usage examples, check out the Davenport.Fsharp.Tests folder which contains tests for every function in the package.
Though rare if your database and indexes are configured properly, CouchDB may return a warning with find
requests (and all those that use them: countBySelector
and existsBySelector
), particularly find
requests that used an index that wasn't configured in your database. Instead of logging to Console.WriteLine, Davenport includes a Warning event on all Configuration objects which you can use to log the message in a way more conducive to your application.
To use it in C#:
var config = new Configuration("http://localhost:5984", "database_name");
var client = new Client<DocumentType>(config);
// Wire up the Warning event:
config.Warning += (object sender, string message) =>
{
// Do whatever you want with the warning message.
Console.WriteLine(message);
};
And to use it in F#:
let client =
"localhost:5984"
|> database "my_database"
|> warning (fun message -> printfn "%s" message)
There are a few different events that will create a warning message:
- (C# and F#) You executed a
Find
operation and looked up documents based on indexes (properties) that weren't configured in your database.- Hint: You can use
Davenport.Configuration.ConfigureDatabaseAsync
in C#, orcreateIndexes
in F#. Using indexes improves the performance of any request that uses theFind
operation (Find
,CountBy
andExistsBy
).
- Hint: You can use
- (C#) Davenport is updating or creating a design doc when configuring a database.
- (C#) Your CouchDB installation is using a version less than 2.0.0.
- If this is the case, you won't be able to use
Find
methods, or any other request that uses theFind
operation (Find
,CountBy
andExistsBy
).
- If this is the case, you won't be able to use