This is the continuation of the material from the previous post of the series: https://dev.to/zoltanhalasz/full-stack-asp-net-core-app-bootcamp-project-part-1-introduction-cfb
Database and entities/models
The main entities of the database will be:
- users: will store the username and their password(not encrypted! bad practice), and their id
- notes: title, content, userid, color
- images: noteid, file name.
Let's have a look at the database script, which defines the relationships.(https://github.com/zoltanhalasz/SmartNotes/blob/master/SmartNotes/script.sql)
Using EF Core, the database is scaffolded into a Model folder.
The model classes will look in the following way(as scaffolded by EF Core):
public partial class Users
{
public Users()
{
Notes = new HashSet<Notes>();
}
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public virtual ICollection<Notes> Notes { get; set; }
// I will use this to store the confirmed password, not save in the DB
[NotMapped]
public string Password2 { get; set; }
}
public partial class Notes
{
public Notes()
{
Images = new HashSet<Images>();
}
public int Id { get; set; }
public int Userid { get; set; }
public string Title { get; set; }
public string NoteText { get; set; }
public DateTime Createdat { get; set; }
public bool Pinned { get; set; }
public string Color { get; set; }
public virtual Users User { get; set; }
public virtual ICollection<Images> Images { get; set; }
}
public partial class Images
{
public int Id { get; set; }
public int Noteid { get; set; }
public string Image { get; set; }
public virtual Notes Note { get; set; }
}
For an existing database, it can be scaffolded into a context and models using the following instructions.(https://www.entityframeworktutorial.net/efcore/create-model-for-existing-database-in-ef-core.aspx)
The Github Repo of my project is here. https://github.com/zoltanhalasz/SmartNotes/
The structure of wwwroot folder:
- css: will contain the manually written css files for each served page
- images: will contain the images that belong to the html of the pages
- js and lib: can be empty.
- uploads: will contain the result of the uploads, images that will appear in each note.
The Pages
The pages are served by Asp.Net Core Razor pages, which is the basic project in Asp.Net Core. (version used here is 3.1, LTS). Each page will have its own css file, present in wwwroot css folder. Their respective html code will be in the cshtml of the Razor Page, only the Notes Page having lots of Javascript included also. I recommend very strongly that you review Razor Pages because they are serving the pages.
The CSS and Html
The Css was written manually (I will not go in detail here) together with the Html, according to a design template. It defines the structure of the files in the view part of the pages below.
Each html file will have its own css. The Layouts will be null in each of the below Razor Pages. You can see the html/css in the Github repo, although this was a part of the bootcamp, I will not go through them.
I will insert more comments here on the blog, than in the Github, to make it more understandable. I will not build the pages step by step, instead just show the comments and explanations regarding the code.
a. Index Page
The PageModel code: - nothing special, here.
The html, you can find on the Github Demo.
b. Signup Page
public class SignUpModel : PageModel
{
private readonly SmartNotesDBContext _context;
public SignUpModel(SmartNotesDBContext context)
{
_context = context;
}
// below property will contain the user that will be created, linked to the page using binding
[BindProperty]
public Users newUser { get; set; }
// below property will be used to display an error message, linked to the page using binding
[BindProperty]
public string errorMessage { get; set; }
// this will display the error message if the user signup did not work well
public void OnGet(string err)
{
errorMessage = err;
}
// basic email validation function
bool IsValidEmail(string email)
{
try
{
var addr = new System.Net.Mail.MailAddress(email);
return addr.Address == email;
}
catch
{
return false;
}
}
// checks if any other user has the same email, which have to be unique in the database.
bool IsExistingEmail(string email)
{
return _context.Users.Any(x => x.Email == email);
}
// posting the form on the SignUp page, and collecting /saving the user data in the database.
public async Task<IActionResult> OnPost()
{
newUser.Email = newUser.Email.Trim();
if (!IsValidEmail(newUser.Email))
{
errorMessage = "Use a valid email address!";
return RedirectToPage("./SignUp", new { err = errorMessage});
}
if (IsExistingEmail(newUser.Email))
{
errorMessage = "This Email Address has already been used!";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
if (newUser.Password!=newUser.Password2)
{
errorMessage = "The passwords do not match!";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
try
{
await _context.Users.AddAsync(newUser);
await _context.SaveChangesAsync();
}
catch (Exception ex)
{
// error message is generated and page redirected
errorMessage = "Error with signup. Please try again later.";
return RedirectToPage("./SignUp", new { err = errorMessage });
}
// when signup was sucessful, user will be redirected to login.
return RedirectToPage("./Login");
}
}
c. Login Page
public class LoginModel : PageModel
{
private readonly SmartNotesDBContext _context;
public LoginModel(SmartNotesDBContext context)
{
_context = context;
}
// the user who tries to log in
[BindProperty]
public Users LoginUser { get; set; }
// the error message which will be shown in the html in case of unsuccesful attempt
[BindProperty]
public string errorMessage { get; set; }
public void OnGet(string err)
{
errorMessage = err;
}
// login, posting the form
public async Task<IActionResult> OnPost()
{
// try to find the user in the table having email and password provided
var myUser = new Users();
try
{
myUser = await _context.Users.FirstAsync(x => x.Email == LoginUser.Email && x.Password == LoginUser.Password);
}
catch (Exception ex)
{
errorMessage = "Invalid User/Password";
// if no user found, error message shown on page in the form.
return RedirectToPage("./Login", new { err = errorMessage });
}
// save the user in the session
SessionHelper.SetObjectAsJson(HttpContext.Session, "loginuser", myUser);
// if user found, it's logged in and redirected to notes.
return RedirectToPage("./Notes");
}
}
d. Notes Page
public class NotesModel : PageModel
{
// this user id will be used in the html/javascript of the page
public int LoginUserID { get; set; }
// this email address will be used in the html/javascript of the page
public string LoginUserEmail { get; set; }
// will take the session value of the logged in user and serve the page. if no user is logged in, will redirect to Login page.
public async Task<IActionResult> OnGet()
{
//check if the user arrived here using the login page, then having the loginuser properly setup
var loginuser = SessionHelper.GetObjectFromJson<Users>(HttpContext.Session, "loginuser");
// if no user logged in using login page, redirect to login
if (loginuser == null)
{
return RedirectToPage("./Login");
}
//just pickup the user id and email to show it on the page (and use them in the js code), see html code
LoginUserID = loginuser.Id;
LoginUserEmail = loginuser.Email;
return Page();
}
}
e. Sign out
public class LogoutModel : PageModel
{
public IActionResult OnGet()
{
// logoout page deleting the logged in user and redirecting to main page.
SessionHelper.SetObjectAsJson(HttpContext.Session, "loginuser", null);
return RedirectToPage("./Index");
}
}
f. Error page
this is mainly a notfound page, written in html/css. Nothing special on the PageModel code.
The Web API back-end of the application will dealt with the CRUD operations, and this will be dealt in the next post.
Top comments (2)
Why do you initialize navigation properties in the constructor as a new HashSet();
Its generated by the scaffolding of model from the db.