-
Notifications
You must be signed in to change notification settings - Fork 4
First steps
Let see how to start to use MongODM with default configurations on an Asp.NET Core project running a local MongoDB Server instance.
You can also find a copy of this sample project here.
As first thing we have to create a new Asp.Net Core project. We will choose the template "Web Application" with support for Razor Pages. From here we can start to work on our application.
MongODM is available as a NuGet packages. We will use the package MongODM
that already include integration with Asp.NET Core and Hangfire task runner.
You can install it with Package Manager Console or with your preferred project editor:
PM> Install-Package MongODM
All dependent packages will be included.
Let's start to configure MongODM inside our solution.
MongODM is designed for work with Domain Model classes, where entities are represented with an Id, and can have relations with property composition between them. It's good for example with DDD pattern.
There are two types of models: Entity
and Value object
.
-
Entity
models must implementIEntityModel<TKey>
interface (this limitation will be removed MODM-2) and can be alterable by methods. It must expose an Id of typeTKey
.IEntityModel<TKey>
also heritage fromIModel
, that implements a MongoDB required fieldExtraElements
(see MongoDB C# Driver documentation) -
Value object
models must implement onlyIModel
interface (also this will be removed) and after construction it become immutable. It can't be altered, and doesn't own an identity. It lives on db only inside an entity model, or another value model.
Visit this page for a complete list of Model restrictions.
It's actually a good practice to implement these two types as abstract base types, and derive all other models from these. So lets add these two classes to our domain:
public abstract class ModelBase : IModel
{
public virtual IDictionary<string, object> ExtraElements { get; protected set; }
}
public abstract class EntityModelBase<TKey> : ModelBase, IEntityModel<TKey>
{
protected EntityModelBase()
{
CreationDateTime = DateTime.Now;
}
public virtual TKey Id { get; protected set; }
public virtual DateTime CreationDateTime { get; protected set; }
public virtual void DisposeForDelete() { }
}
ModelBase
is a relative simple class. It implements IModel
with its required property IDictionary<string, object> ExtraElements
, and it's declared as abstract because its only base for other models, but already here we can note 2 important things:
- Every property and methods in our models have to be declared
virtual
. This because MongODM make use of Proxy pattern implemented with Castle.Core for perform several actions. - Every property that have to be writable by serializer must have a setter, but it can be also non pubblic. Currently only
protected
andpublic
declarations are supported, but in future alsoprivate
will be available MODM-23
EntityModelBase<TKey>
instead exposes more things. At first it implements IEntityModel<TKey>
which heritage IModel
, so we can directly heritage from ModelBase
for avoid to repeat ExtraElements
declaration.
Moreover this class exposes two other properties: TKey Id { get; protected set; }
and DateTime CreationDateTime { get; protected set; }
. Both of them are virtual and have protected setters. Id is of generic type TKey
, so each entity model will be able to use the preferred type. CreationDateTime
is instantiated with current date/time in constructor, but Id
isn't. In fact, Id will be automatically set by MongoDB drivers when model will be created as a document in db, if a value hasn't been set. It's used reflection in this operation, so also not public setter is fine.
The method void DisposeForDelete() { }
is invoked whenever the model is required for deletion from db, and its references have to be removed from other objects that still remain, but will be probably removed in future releases for be replaced with an easier and autonomous way. It can be empty here.
At this point, we can start to create effective domain models. I love cats, so in this sample we will implement a really simple register of cats.
public class Cat : EntityModelBase<string>
{
public Cat(string name, DateTime birthday)
{
Name = name;
Birthday = birthday;
}
protected Cat() { }
public virtual int Age => (int)((DateTime.Now - Birthday).TotalDays / 365);
public virtual DateTime Birthday { get; protected set; }
public virtual string Name { get; protected set; }
}
Our Cat
class heritage from EntityModelBase<string>
, so it has a string Id
. After this, a public
constructor take a name and a birthday date for create a consistent new instance of a Cat, but a protected
empty constructor is also declared. This is needed because when serializator will try to read the model from a document, it needs a way for create a new empty Cat
instance and fill it with retrieved data. The empty constructor gives this way.
Birthday
and Name
properties don't have nothing of special, but Age
property is missing of a setter. Because of this, serializer will ignore to report it by default on serialized documents.
For this very easy sample, our domain models are completed.
The database context is a provider of repositories that administrate data around a domain defined boundary. Each repository works directly only with Entity models, so every CRUD operation can be performed only with models owning an Id.
Each database context has to heritage from DbContext
abstract class, which implement the IDbContext
interface. Naming of database context is arbitrary to application, but is advised that it describes the domain boundary. Using of an interface exposing the domain repositories is not mandatory, but also this advised for be used with Dependency Injection systems, and MongODM library supports this.
So let's add an interface and a class implementing our database context, describing domain boundary of our sample application:
public interface ISampleDbContext : IDbContext
{
ICollectionRepository<Cat, string> Cats { get; }
}
public class SampleDbContext : DbContext, ISampleDbContext
{
public SampleDbContext(
IDbDependencies dependencies,
DbContextOptions<SampleDbContext> options)
: base(dependencies, options)
{ }
public ICollectionRepository<Cat, string> Cats { get; } = new CollectionRepository<Cat, string>("cats");
protected override IEnumerable<IModelMapsCollector> ModelMapsCollectors { get; }
}
The interface ISampleDbContext
heritages from IDbContext
, so injecting it we will have directly access to methods implemented by db context. It exposes also the property ICollectionRepository<Cat, string> Cats { get; }
. It is a Collection repository for the model type Cat
, which uses a string
Id. It is a collection of cats, so it's named Cats
, and is readonly.
The class SampleDbContext
heritages from DbContext
and implements ISampleDbContext
. The constructor takes two arguments:
-
IDbDependencies dependencies
inject all the required dependencies forDbContext
. They are injected for be compliant to Dependency Injection pattern requirements, but they are also grouped into this type for avoid to have a long list of passed arguments -
DbContextOptions<SampleDbContext> options
are the options for the database context, they have to be passed to the base constructor. Injecting them into constructor we can take advantage of the dependency injection system and perform all configuration onStartup
class. The generic type argument is the same type of the database context class, and it's used for differentiate different options for different contexts.
The collection repository here is instantiate with new CollectionRepository<Cat, string>("cats")
, which can take the name of the collection on db as parameter, or an instance of CollectionRepositoryOptions<TModel>
for more options.
The overridden property IEnumerable<IModelMapsCollector> ModelMapsCollectors { get; }
is necessary, and will enumerate all our model maps collectors that have to be registered. This describe how data will be serialized in documents, keep it uninitialized for now.
Now we are going to create our first model map. Model maps are descriptors used by serializators for understand how a model has to be treated when it has to be wrote or read on documents.
MongODM uses the serialization system provided by the official MongoDB's C# drivers, so it does an estense use of model mapping (class mapping in MongoDB's jargon). What we have to do is to create an instance of IModelMapsCollector
for each model type (or logical group of types), and inside of them we will define how to serialize the models, and optionally all their summary versions used for denormalizated references.
Is not mandatory to have different model maps collectors, but this can help to keep model maps organized. Let's add these classes to our project:
class ModelBaseMap : IModelMapsCollector
{
public void Register(IDbContext dbContext)
{
dbContext.SchemaRegister.AddModelMapsSchema<ModelBase>("1252861f-82d9-4c72-975e-3571d5e1b6e6");
dbContext.SchemaRegister.AddModelMapsSchema<EntityModelBase<string>>("81dd8b35-a0af-44d9-80b4-ab7ae9844eb5", modelMap =>
{
modelMap.AutoMap();
modelMap.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId))
.SetIdGenerator(new StringObjectIdGenerator());
});
}
}
class CatMap : IModelMapsCollector
{
public void Register(IDbContext dbContext)
{
dbContext.SchemaRegister.AddModelMapsSchema<Cat>("cd37bafa-a36d-4b1f-815a-deb50c49d030");
}
}
The class ModelBaseMap
contains model maps for ModelBase
and EntityModelBase<string>
. These two types are in fact the common base models for our domain. IModelMapsCollector
require implementation of method void Register(IDbContext dbContext)
, here our models maps are registered. The parameter dbContext
is relative to current dbContext where we are going to register maps, and can be useful for access to different components during registrations.
Registration has to be done with SchemaRegister
, it is a transient instance of a register containing all schemas used into the current database context. We have to invoke the AddModelMapsSchema<TModel>()
method for add a new model maps schema. The generic argument describes the model type that we are going to register.
Registering a model maps schema we have to provide information about the current active model map for the model, and optionally we can also add information about secondary maps. Every model maps schema is a collector of model maps for a specific model type. The current active map is used for document serialization and deserialization, instead any secondary schema is used only for deserialization.
The first (or maybe only) parameter for register a new model maps schema is the unique active map's Id. In fact any model map has to have an immutable and unique id. It's used during deserialization for understand what instance of model map to use. This multi model maps support avoids to have to align all documents to current schema, and so avoid to perform a lot of migrations (one big point for documental!). In this case, because a model map id is any type of not-empty string, we are using random GUIDs.
The second and optional parameter is the definition of the model map. Currently it's used the MongoDB's BsonClassMap
implementation, in future probably will be provided a custom one. If a model map is not configured, by default it's created one with the only invocation of modelMap.AutoMap();
. For more information on how to use BsonClassMap
, we link here the official documentation about it.
On the second collector CatMap
we are simply mapping the model Cat
using the default AutoMap()
method provided by BsonClassMap
.
Now we can go back to our dbContext and update the property ModelMapsCollectors
as follows:
protected override IEnumerable<IModelMapsCollector> ModelMapsCollectors =>
new IModelMapsCollector[]
{
new ModelBaseMap(),
new CatMap()
};
Now it's time to register MongODM components into our project. By design MongoDB's components are very decoupled, an advantage of this is that they are interchangeable with others provided by users, but for avoid to have to configure each one of them, MongODM provide also a configurator that assume to use it with default Asp.NET Core and Hangfire.
In our Startup
class we have the method ConfigureServices(IServiceCollection services)
where we can register components to the dependency injection system. We will use here the simplest configuration with default components, so add this code to the method:
services.AddMongODMWithHangfire<ModelBase>()
.AddDbContext<ISampleDbContext, SampleDbContext>();
Here we are using cascade configuration for set our MongODM environment with Hangfire. This configuration will use a local running instance of MongoDB.
With AddMongODMWithHangfire
we are configuring global settings. The generic parameter represent our lowest model base type, it's needed because of this issue on MongoDB's official drivers CSHARP-3154.
The method AddDbContext<ISampleDbContext, SampleDbContext>()
instead register our database context and its interface into the dependency injection system. Currently only one database contexts are supported at time (MODM-18), but in future we will be able to configure more.
At this point we have to enable an Hangfire server for perform execution of scheduled tasks. We will run it locally inside the same application, but this is not the only available configuration. For more info please look at official Hangfire documentation.
Into Startup
class find the method Configure(IApplicationBuilder app)
(it could have more parameters), and add this code:
app.UseHangfireServer();
It can be added just before app.UseEndpoints(...)
or where you prefer, but pay attention because here invoke position is important. For more information on how to configure this method, look at Asp.NET Core documentation
Optionally you can also add this method call for enable Hangfire's Dashboard:
app.UseHangfireDashboard();
For more info we refer to Hangfire's documentation.
Optionally we can also use a default administration dashboard for MongODM. We can add it importing in project the package MongODM.AspNetCore.UI
:
PM> Install-Package MongODM.AspNetCore.UI
After this we have to add this line in method ConfigureServices(IServiceCollection services)
for configure the dashbord options:
services.AddMongODMAdminDashboard();
In Configure(IApplicationBuilder app)
method we also need to add to the pipeline (if not already present) the Asp.NET's Authorization middleware. The dashboard use it for manage authorizations. It's advised to add it between app.UseRouting()
and app.UseEndpoints(...)
invokes:
app.UseAuthorization();
At this point, we can easily find the dashboard routing our browser to the default path /mongodm
.
We have configured now our MongODM environment. We can start to develop a simple application with it. From our Asp.Net Core application template, go to replace the view file Index.cshtml
with this:
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div class="row">
<div class="col-md-4">
<form method="post">
<div class="form-group">
<label asp-for="Input.Name"></label>
<input asp-for="Input.Name" class="form-control" />
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Input.Birthday"></label>
<input asp-for="Input.Birthday" class="form-control" />
<span asp-validation-for="Input.Birthday" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Add cat</button>
</div>
</form>
</div>
<div class="col-md-6 col-md-offset-2">
<section>
<h4>List of cats</h4>
<form method="post">
<div>
@foreach (var cat in Model.Cats)
{
<p>
<label asp-for="@cat.Name"></label>
<input asp-for="@cat.Name" disabled />
<label asp-for="@cat.Birthday"></label>
<input asp-for="@cat.Birthday" type="date" disabled />
<button type="submit" class="btn btn-link" asp-page-handler="Remove" asp-route-id="@cat.Id">X</button>
</p>
}
</div>
</form>
</section>
</div>
</div>
and the IndexModel
class in Index.cshtml.cs
with this:
public class IndexModel : PageModel
{
// Models.
public class InputModel
{
[Required]
[DataType(DataType.Date)]
public DateTime Birthday { get; set; }
[Required]
public string Name { get; set; }
}
// Fields.
private readonly ISampleDbContext sampleDbContext;
// Constructor.
public IndexModel(ISampleDbContext sampleDbContext)
{
this.sampleDbContext = sampleDbContext;
}
// Properties.
public List<Cat> Cats { get; } = new List<Cat>();
[BindProperty]
public InputModel Input { get; set; }
// Methods.
public async Task<IActionResult> OnGetAsync()
{
await LoadCats();
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
await LoadCats();
if (!ModelState.IsValid)
return Page();
var cat = new Cat(Input.Name, Input.Birthday);
await sampleDbContext.Cats.CreateAsync(cat);
return RedirectToPage();
}
public async Task<IActionResult> OnPostRemoveAsync(string id)
{
await sampleDbContext.Cats.DeleteAsync(id);
return RedirectToPage();
}
// Private helpers.
private async Task LoadCats()
{
var cats = await sampleDbContext.Cats.QueryElementsAsync(elements =>
elements.ToListAsync());
Cats.AddRange(cats);
}
}
Here we will not concentrate on how Asp.Net Core Razor Pages works, let see how MongODM is involved.
From IndexModel
constructor we are injecting an ISampleDbContext sampleDbContext
instance. This is configured by a singleton in our application. So the constructor saves it as a field.
From methods OnGetAsync()
and OnPostAsync()
we invoke the method LoadCatsAsync()
. This uses our database context for access to Cats
repository, and run a LINQ query on repository elements, ending with ToListAsync()
. This will return asynchronously all the cats on db. Our cats from database are so added to the Cats
list outgoing to the view.
On method OnPostAsync()
we can also see how to create a new entity. We simply are going to create a new Cat(Input.Name, Input.Birthday)
instance, and this is passed to CreateAsync(cat)
method on Cats
repository.
The last method OnPostRemoveAsync(string id)
is even simpler, because we have only to invoke method DeleteAsync(id)
from the repository.
This was simply an "Hello world" on how to start to work with MongODM. Source code is available in repository, and this is the final result: