Introduction
By now, you've probably heard about NoSQL databases. It's something that's been very fashionable lately in the development world.
At present we are used to SQL databases like MySQL, Oracle, or SQL Server, but others, called NoSQL (Not only SQL), have appeared, and that has come with the intention of dealing with the bases related used by most users.
In short, NoSQL databases differ in several aspects of lifelong relational databases such as they can handle a large amount of data, no fixed structures (tables, columns, etc.) are needed for data storage, they do not usually allow JOIN operations, among other aspects.
There are several types of NoSQL databases that we could group together different types: documentaries, graphs, key/value, multivalue, object-oriented, or tabular.
Taking advantage of the growing popularity of the .NET ecosystem, in our case, we will learn how to make web applications in ASP.NET Core with the MVVM design pattern (Model-View-Viewmodel) thanks to DotVVM and through document databases with MongoDB.
Resources and tools needed
The solution environment
For our case study, we will take as an example document the information of a student to develop CRUD operations and manage the application in three parts:
- Data Access Layer Implementation: to manage connection and access to the document database in MongoDB.
- Implementation of the BL (Business Layer): for the management of services and logic of the application domain.
- Implementation of the application presentation layer. This section is where DotVVM comes into action.
Part 1 - Data Access Layer - DAL
To work with MongoDB you need to install the following NuGet package,
MongoDB.Driver
As the first point to create our application, in the 'Data Access Layer' we must define the collections that the application domain will have and define a configuration module to reference the properties of the database (connection string, name of the database, and database collections).
In the application, we will handle the collection: Student. If we had other collections, for example: Professor, Subject, etc; these will be located in the Collections folder. For our case study, the Student collection will be defined as follows:
- public class Student
- {
- [BsonId]
- [BsonRepresentation(BsonType.Int32)]
- [BsonElement("_id")]
- public int Id { get; set; }
-
- [BsonElement("FirstName")]
- public string FirstName { get; set; }
-
- [BsonElement("LastName")]
- public string LastName { get; set; }
-
- [BsonElement("About")]
- public string About { get; set; }
-
- [BsonElement("EnrollmentDate")]
- [BsonRepresentation(BsonType.DateTime)]
- public DateTime EnrollmentDate { get; set; }
- }
BSON format is used for storage and data transfer in MongoDB, which allows us to work with documents in MongoDB. In this case, in the Student class, we can specify the name of the attribute of the collection we refer to (usually used when the names are different), the BSON data type, and other directives. For example, in MongoDB, the primary key of a collection is identified by the attribute: _id, which will allow us to identify a document, in this case, the Id of the student will be the primary identifier. The BSON data types can be found in the MongoDB documentation - https://docs.mongodb.com/manual/reference/bson-types/.
On the other hand, we have the interface and its implementation: DatabaseSettings for handling the specific properties of the database with MongoDB,
- public class DatabaseSettings : IDatabaseSettings
- {
- public string CollectionName { get; set; }
- public string ConnectionString { get; set; }
- public string DatabaseName { get; set; }
- }
-
- public interface IDatabaseSettings
- {
- string CollectionName { get; set; }
- string ConnectionString { get; set; }
- string DatabaseName { get; set; }
- }
The values of each of the attributes in DatabaseSettings will be defined in the appsettings.json file. The names of the JSON and C-properties are the same to make the mapping process easier.
appsettings.json file
- {
- "DatabaseSettings": {
- "CollectionName": "Student",
- "ConnectionString": "mongodb://...",
- "DatabaseName": "StudentDb"
- }
- }
To establish the relation between the DatabaseSettings class and the appsettings.json configuration file, we need to define this setting in the constructor and in the ConfigureServices method of the Startup.cs class as shown below:
- public IConfiguration Configuration { get; private set; }
-
- public Startup(IHostingEnvironment env)
- {
-
- var builder = new ConfigurationBuilder()
- .SetBasePath(env.ContentRootPath)
- .AddJsonFile("appsettings.json");
-
- builder.AddEnvironmentVariables();
- Configuration = builder.Build();
- }
- public IConfiguration Configuration { get; private set; }
-
- public Startup(IHostingEnvironment env)
- {
-
- var builder = new ConfigurationBuilder()
- .SetBasePath(env.ContentRootPath)
- .AddJsonFile("appsettings.json");
-
- builder.AddEnvironmentVariables();
- Configuration = builder.Build();
- }
There are two important aspects to say this code:
-
The configuration instance to which the DatabaseSettings section of the appsettings.json file links is registered in the dependency injection container. For example, a ConnectionString property of the DatabaseSettings object is populated with the DatabaseSettings:ConnectionString property in appsettings.json.
-
The DatabaseSettings interface is registered in dependency injection singleton service life (design pattern). When inserted, the interface instance resolves to a DatabaseSettings object.
Part 2 - Business Layer - BL
Now we need to define the models and create the services to handle the logic of our application. In this case, what we are looking for is to have a general list of students and the specific information of each of them.
Models
To do this, as a first point we will define our models:
- public class StudentListModel
- {
- public int Id {get; set;}
- public string FirstName {get; set;}
- public string LastName {get; set;}
- }
- public class StudentDetailModel
- {
- public int Id { get; set; }
-
- [Required]
- public string FirstName { get; set; }
-
- [Required]
- public string LastName { get; set; }
-
- [Required]
- public DateTime EnrollmentDate {get; set;}
-
- public string About { get; set; }
- }
Services
In this case, we have the 'Students' service that will allow us to implement CRUD operations: Create, Read, Update, and Delete documents in MongoDB. Next, let's look at each of them.
A. Service initialization
- private readonly IMongoCollection<Student> _students;
-
- public StudentService(IDatabaseSettings settings)
- {
- var client = new MongoClient(settings.ConnectionString);
- var database = client.GetDatabase(settings.DatabaseName);
-
- _students = database.GetCollection<Student>(settings.CollectionName);
- }
In short, in this part the work done is to establish the connection with MongoDB, selecting the database with which we will work and the corresponding collection.
B. Read documents
- public List<StudentListModel> GetAllStudents()
- {
- return _students.Find(Student => true).ToList().Select(
- s => new StudentListModel
- {
- Id = s.Id,
- FirstName = s.FirstName,
- LastName = s.LastName
- }
- ).ToList();
- }
The first option we have is to get all the documents from the collection we are working with. Through the _students attribute that refers to the collection, we can use the
methods provided by MongoDB to perform CRUD, query, and other operations.
In this case, the Find() method is used to query documents in a collection. With the result of this query, we can use LINQ (Language Integrated Query), a component in which we can query through objects (very similar to SQL). LINQ will help us to set a list of objects of type StudentListModel with the query result documents in MongoDB.
We can also query with conditionals, for example, see the following query:
- public async Task<StudentDetailModel> GetStudentByIdAsync(int studentId)
- {
- Student document = await _students.FindAsync<Student>(Student => Student.Id == studentId).Result.FirstOrDefaultAsync();
-
- StudentDetailModel student = new StudentDetailModel();
- student.Id = document.Id;
- student.FirstName = document.FirstName;
- student.LastName = document.LastName;
- student.About = document.About;
- student.EnrollmentDate = document.EnrollmentDate;
-
- return student;
- }
For the creation of documents in a collection we have the methods: InsertOne, for inserting a document; and InsertMany, for the insertion of several of them. For this case, the method that we have intended for this purpose InsertStudentAsync receives a model StudentDetailModel with the data to be inserted, the process consists in constructing a Student class (the one that references the database) and inserting it then through the reference _students.
- public async Task InsertStudentAsync(StudentDetailModel student)
- {
- var document = new Student()
- {
- Id = student.Id,
- FirstName = student.FirstName,
- LastName = student.LastName,
- About = student.About,
- EnrollmentDate = student.EnrollmentDate
- };
- await _students.InsertOneAsync(document);
- }
The same logic of the creation and insert methods is followed for document updates. The reference document will be updated according to the ID of the reference document.
- public async Task UpdateStudentAsync(StudentDetailModel student)
- {
- var document = new Student()
- {
- Id = student.Id,
- FirstName = student.FirstName,
- LastName = student.LastName,
- About = student.About,
- EnrollmentDate = student.EnrollmentDate
- };
-
- await _students.ReplaceOneAsync(Student => Student.Id == student.Id, document);
- }
To delete a document, the DeleteOne method is used, which allows you to delete a document according to the document ID.
- public async Task DeleteStudentAsync(int IdStudent)
- {
- await _students.DeleteOneAsync(Student => Student.Id == IdStudent);
- }
So far, we have defined the services of our application domain model. What remains is to use these services from an application, which will be designed in ASP.NET Core with DotVVM.
Part 3 - Application Presentation Layer
Now that we have defined the DAL and the BL, we must now perform the design of the website so that the user can interact with it and in this case, perform CRUD operations for the management of Students.
Before proceeding, it is necessary to associate with our application (register) the services that will be used. For this, we should look to the ConfigureServices method in the Startup class and have something like this:
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddDataProtection();
- services.AddAuthorization();
- services.AddWebEncoders();
- services.AddTransient(typeof(StudentService));
- services.Configure<DatabaseSettings>(Configuration.GetSection(nameof(DatabaseSettings)));
- services.AddSingleton<IDatabaseSettings>(sp => sp.GetRequiredService<IOptions<DatabaseSettings>>().Value);
- services.AddDotVVM<DotvvmStartup>();
- }
Now, this is the part where DotVVM comes into action. Each page in DotVVM consists of two files:
- A view, which is based on HTML syntax and describes how the page will look.
- A view model, which is a class in CTM that describes the state of the page (for example, values in the form fields) and handles user interactions (for example, button clicks).
In our application, we will have four Views and four models associated with these views,
- Default: will be the main page of the application where the list of registered students will be displayed.
- Create: A page made up of a form to create new students.
- Detail: to view a student's information in detail.
- Edit: to modify a student's information or delete it.
Considering the Views and Viewmodels files, in Visual Studio, we'll see something like this:
Next, let's take a closer look at the View and Viewmodel of Default and its components.
Default viewmodel
- public class DefaultViewModel : MasterPageViewModel
- {
- private readonly StudentService studentService;
-
- public DefaultViewModel(StudentService studentService)
- {
- this.studentService = studentService;
- }
-
- [Bind(Direction.ServerToClient)]
- public List<StudentListModel> Students { get; set; }
-
- public override async Task PreRender()
- {
- Students = await studentService.GetAllStudentsAsync();
- await base.PreRender();
- }
- }
As a first point, we have the StudentService instance that will allow us to access the methods to handle the operations defined in the Student service implemented in the BL.
Then, we have the definition List<StudentListModel> Students of type StudentListModel defined in the model classes in the BL, which will have a list of students (Id, FirstName and LastName) to load them into a table on the main page of the web application.
A very important feature to mention is the [Bind(Direction.ServerToClient)] declaration. These types of properties allow you to specify which information is to be transferred from the server to the client or from the client to the server when using Binding Directions. Considering the case of the student list, in many cases, it is not necessary to transfer the entire view model in both directions. From the server to the eye will suffice in this case.
Learn more about Binding Directions
here.
Finally, in the Viewmodel of Default, we have the PreRender() method, which allows you to perform certain types of operations that will be performed when loading the View. In this case, a query will be made to the database by calling the service method studentService.GetAllStudentsAsync(), then the results will be assigned in the Students collection of type StudentListModel and then the page will be loaded along with the other design components.
Default view
- <dot:Content ContentPlaceHolderID="MainContent">
- <div class="page-center">
- <div class="page-grid-top">
- <div class="student-image"></div>
- <h1>Student List</h1>
- <dot:RouteLink Text="New Student" RouteName="CRUD_Create" class="page-button btn-add btn-long"/>
- </div>
- <dot:GridView DataSource="{value: Students}" class="page-grid">
- <Columns>
- <dot:GridViewTextColumn ValueBinding="{value: Id}" HeaderText="Id" />
- <dot:GridViewTextColumn ValueBinding="{value: FirstName}" HeaderText="Firstname" />
- <dot:GridViewTextColumn ValueBinding="{value: LastName}" HeaderText="Lastname" />
- <dot:GridViewTemplateColumn>
- <dot:RouteLink Text="Detail" RouteName="CRUD_Detail" Param-Id="{{value: Id}}" />
- </dot:GridViewTemplateColumn>
- <dot:GridViewTemplateColumn>
- <dot:RouteLink Text="Edit" RouteName="CRUD_Edit" Param-Id="{{value: Id}}" />
- </dot:GridViewTemplateColumn>
- </Columns>
- <EmptyDataTemplate>
- There are no registered students. First sign in or sign up and add some students.
- </EmptyDataTemplate>
- </dot:GridView>
- </div>
- </dot:Content>
As we can see from the Default View, the layout of the page becomes the handling of HTML and CSS statements. For our case study, there are some interesting statements and features that we can analyze,
GridView: <dot:GridView ... >, a DotVVM control that allows us to create a table or grid to display a certain list of information. In HTML we would be talking about the <table> tag. One of its attributes is DataSource: DataSource="{value: Students}", which allows you to specify the data source, in this case, we refer to the list of students: Students, which was defined in the Viewmodel as we saw earlier.
In addition to grids, DotVVM also has other custom control components, for example, for text boxes, ComboBox, file handling, among others that allow us to maintain communication between the View and the sources of information defined in Viewmodels. See more
here.
Continuing our analysis, in the GridView, we have the columns Id, FirstName, and LastName of the students, but additionally, we can also add columns to perform operations on a specific record. In this case, with RouteLink, we can define a hyperlink that constructs a URL from path names and parameter values to redirect us to other pages or perform additional operations, for example, view detail or modify the record of a student in particular according to their ID:
- <dot:RouteLink RouteName="Edit" Param-Id="{{value: Id}}" />
These paths and their corresponding parameters must be defined in the file DotvvmStartup.cs in the ConfigureRoutes method as follows,
- config.RouteTable.Add("Edit", "edit/{Id}", "Views/Edit.dothtml");
To learn more about routing in DotVVM, you can go
here.
The Create, View Detail, and Modify pages follow the same logic for the View and Viewmodel components. When you add some student records in our app and load the homepage with the list of them, we'll have something like this:
Plus - Resources for hosting MongoDB data and NoSQL databases in the cloud
Today the trend is in publishing web pages in the cloud, for this, there are several services that allow us to meet these objectives, whatever database manager is being used, whether SQL or NoSQL. For NoSQL databases, we have these recommended options:
A. mLab
mLab is a free managed cloud database service that hosts MongoDB databases. It has the same local MongoDB operation, the only thing that changes is the connection string. To learn more about mLab you can go to https://mlab.com/.
B. Azure CosmosDB - The API for MongoDB
Azure Cosmos DB is a multi-model, global Microsoft distribution database service. For this case, Azure provides a Cosmos DB API for MongoDB, which, in addition to working with all the capabilities of MongoDB, we can also use the full power of Azure for handling NoSQL databases, for example, global distribution, automatic partitioning, availability and latency guarantees, encryption at rest, backups, among others. To learn more about the Cosmos DB API for MongoDB we can visit this
Microsoft document.
What's next?
With this tutorial, we learned how to work with MongoDB in applications with ASP.NET Core and DotVVM through Visual Studio 2019 to implement CRUD operations on a collection of Students.
The code in this tutorial can be found in the following repository on GitHub -
DotVVM + MongoDB.
Thank you!