Skip to content

Documents

Shannon Lewis edited this page Jul 7, 2021 · 6 revisions

In Nevermore, a document is a class with a DocumentMap. The DocumentMap helps Nevermore to understand all the things it needs to treat your class as a document: what the ID property is, which properties should be stored as columns vs. in JSON, and so on.

A simple document map

You create a class that inherits the generic DocumentMap class, passing your document class as a parameter. Most document maps should look quite simple.

class CustomerMap : DocumentMap<Customer>
{
    public CustomerMap() 
    {
        Column(c => c.FirstName);
        Column(c => c.LastName);
    }
}

The default conventions are:

  • If you have a property called "Id", it will be mapped automatically
  • Id values will be generated automatically, and formatted as TableNames-{0} (e.g., Customers-123)
  • The table name will be your class name ("Customer" in this example)

Everything else in the class will be serialized as JSON.

Here's a big document map showing all the options:

public class CustomerMap : DocumentMap<Customer>
{
    public CustomerMap()
    {
        // Name of the table this document is saved to (defaults to class name)
        TableName = "TblCustomers";
        
        // Use something like "CUST0000001292" for ID's instead of "Customers-123" (default)
        IdFormat = i => "C" + i.ToString("n0").PadLeft(10, '0');
        
        Id().MaxLength(100);

        // Max length prevents you saving the record with a bigger string - it will throw a friendly
        // exception and tell you which property was at fault - which SQL doesn't do!
        Column(m => m.FirstName).MaxLength(200);        
        Column(m => m.Nickname).MaxLength(200);
        
        // Custom property handlers let you control how the property is read/written from the database
        Column(m => m.Roles).CustomPropertyHandler(new MyCustomPropertyHandler());
        
        // Read from the database only, Nevermore won't attempt to insert/update it. Use this for 
        // values that are calculated or changed only within SQL Server.
        Column(m => m.RowVersion).LoadOnly();    

        // Save to the database only, Nevermore won't attempt to read it when querying
        // Use this for properties that are calculated in .NET and useful to query against
        Column(m => m.HasNickname).SaveOnly();    
        
        Unique("UniqueCustomerNames", new[] { "FirstName", "LastName" }, "Customers must have a unique name");
        
        // Default false. See performance page for details. 
        ExpectLargeDocuments = true;

        // See the compression page for details
        JsonStorageFormat = JsonStorageFormat.MixedPreferCompressed;
    }
}

Property accessors

Every property you map to a column will need a public get accessor. The set accessor is optional:

  • If there's no set accessor at all, Nevermore will require you to identify the column as SaveOnly()
  • The set accessor can be private or protected, and Nevermore will call it

This allows you to design properties in a few ways:

  • If a property is calculated, it can be stored in a column, and just have a get accessor
  • If a property is usually only set in the constructor or upon creation, it can have a private set accessor. This prevents you from setting it outside of the class in code, but Nevermore can still set it when it loads an object

Table requirements

Your tables will typically look like this:

  • The Id column, as a non-null column and primary key
  • The other columns you want to map
  • The JSON (and/or JSONBlob if you use compression) column

For example:

create table Customer (
    Id nvarchar(50) not null constraint PK_Customer_Id primary key,
    -- ... other columns here ...
    [JSON] nvarchar(max) not null
)

Id column

Every table should have an Id column. It's possible to use a custom column name or property name (see below), but you should try to stick with this convention.

For string Ids, Nevermore assumes columns are nvarchar(50). If you use a different length (for example, to support longer keys) you can configure it in the document map:

public CustomerMap()
{
    Id().MaxLength(200);
    Column(c => C.FirstName); 
    ...
}

If you break from the convention and want to give the column a different name, you can map it like this:

public CustomerMap()
{
    Id(c => c.CustomerId, "Customer_Id");
    ...
}

Nevermore also supports Id types of string, int, long, and Guid out of the box. For more details on how to extend that support see Primary Key Handlers.

JSON columns

Nevermore lets you store JSON either as text, or compressed, or a mix of the two to allow for migrations. See the section on compression to learn why you might want to use it.

Every table used for documents needs a JSON column, a JSONBlob column, or both. Those names have special meaning and cannot be changed.

The [JSON] column must be nvarchar(max). It's used when JsonStorageMode is TextOnly (the default) or one of the mixed options.

The [JSONBlob] column must be varbinary(max). It's used when JsonStorageMode is CompressedOnly or one of the mixed options.

If you have both columns (because you use one of the mixed JsonStorageMode options, again see compression), then you should set the columns as null:

create table Customer (
    -- ...
    [JSON] nvarchar(max) null,
    [JSONBlob] varbinary(max) null
)

Type column

A column with the name Type has special meaning - it tells Nevermore to look for an IInstanceTypeResolver. If it exists, the Type column must appear before the JSON columns in the select clause.

The column can be any type (though it's typically an nvarchar) as long as there is an ITypeHandler that can map it to a CLR type. See Instance Type Resolvers for more details on how the Type column works.

Other columns

All other columns should be mapped using Column. Here are some of the options for a column:

MaxLength

If you try to insert a string into SQL Server, and the string is too long for the type, you'll receive an error. However, SQL Server doesn't make it particularly clear which string was at fault. You might have a dozen string properties on the table.

For properties named "Id" or that end with "Id" (foreign keys etc.), the max length will default to 50. Otherwise, the default is null.

If a max length is specified, and you try to insert a value larger than the max length, Nevermore will throw a helpful StringTooLongException with the name of the column.

If the max length is null (default for non-Id columns) then you'll receive the error from SQL instead. So it's a good practice to add MaxLength properties to your document map.

Unique constraints

You can add indexes, computed columns, and all other types of things to your table, and most of this doesn't need to appear in the document map.

If you add unique constraints to the table, you can tell Nevermore about this (though you don't have to). If you do, and an exception is thrown when inserting a record, Nevermore will try to translate the error message to something more useful. For example:

ALTER TABLE [Person] ADD CONSTRAINT [UQ_UniquePersonEmail] UNIQUE([Email])
class PersonMap : DocumentMap<Person>
{
    public PersonMap()
    {
        Column(m => m.FirstName).MaxLength(20);
        Column(m => m.LastName).Nullable();
        Column(m => m.Email);
        Unique("UniquePersonEmail", new[] { "Email" }, "People must have unique emails");
    }
}

In this example, if you insert a person with an email that is already in use, SQL will return an error code with the name of the constraint. Nevermore will attempt to find a Unique rule on your document map with a name that matches the unique constraint name from SQL (it's a partial match - the string you pass to Unique needs to be contained somewhere in the unique constraint name).

If Nevermore matches it, you'll get a friendly UniqueConstraintViolationException with the message "People must have unique emails". This is much more useful than the generic error from SqlCommand would be.

Examples for common columns

RowVersion

If you use RowVersion, declare the property as either a byte[] or Int64/long (rowversions are always 8 bytes). Alternatively, you might want to create a custom type, and supply a type handler (there's a bit of detail).