6.2 KiB
+++ categories = ["software"] tags = ["C#","MSSQL","blazor"] date = 2024-11-02T18:24:31Z description = "" draft = false slug = "formulation-web-app-I" title = "🔒 Adding User Account support to Web App" author = "nicholas" +++
All of the changes I make to the formulation repository are tracked in my private git SCM, Gitea. This log will mostly only ever reference those changes so that I do not repeat the code here, in keeping with the principle of DRY.
Discussion
I built a simple app to manage my recipes. As of writing this post, the app only has minimal functionality, but it does everything I wanted it to do when I started the project. Before I decide to add additional features, I reason it will be good to add user account support first, mostly because this feature will touch almost every aspect of the project. Every additional feature will be constrained by the user account feature – pages will need authentication, and running certain operations will need proper authorization, database changes, etc. It is best to get this out of the way first.
First Steps - ASP.NET Core Identity
First I look for precedent in implementing user accounts into a Blazor Webassembly app. Happily I have found Microsoft's very own example here: https://github.com/dotnet/blazor-samples/tree/main/8.0/BlazorWebAssemblyStandaloneWithIdentity
This example uses ASP.NET Core Identity, an API that supports UI login functionality and manages users, passwords, profile data, roles, claims, tokens, email confirmation, and more. This system of authentication and authorization will slot into my own project with a few minimally tedious changes, which I do not outline here. Alternatively, I could use an external credential system like OIDC to allow users to authenticate using external credentials from Microsoft, Google, etc. I decided not to go this route because it is generally more complicated to implement, and I have no real need.
The state of things
After some time I arrive at working implementation of Identity:
-
An unauthenticated, non-authorized user visits the
homepage. This page is configured to only show content when the user has proper authorization. {{< image src="images/notauthorized.png" caption="Not Authorized" >}} -
User visits the
/registerpage to create an account. The supplied credentials are stored in the database. {{< image src="images/register.png" caption="Register Page" >}} {{< image src="images/register_success.png" caption="Registration Successful" >}} -
User visits the
/loginpage and enters previously created credentials to be matched against those stored in the database. {{< image src="images/login.png" caption="Login" >}} {{< image src="images/login-success.png" caption="Login Success" >}} -
User logs in successfully and is able to visit
/homepage and see the protected content.
{{< image src="images/authorized.png" caption="Authorized User" >}}
For now, that is all I need. I now have a functional and secure implementation of a user membership system that I can build around.
Entity Framework Code-First Approach
The next step is to streamline the development process for the database. For this I will use the "code-first" approach to Entity Framework. This allows me to define my database schema in C# code instead of designing the database first. This is especially useful when the ultimate requirements of the database are not well understood, which is common during the early, rapidly iterative development process.
This approach frees me to focus on mostly just the code and leaves the tedious task of continuously dropping and modifying databases – rewriting the entire SQL query that generates the database, to the computer using EF Core Package Manager Console Tools. This is a command-line tool that is already built in to Visual Studio.
Here is the basic process:
- I decide I do not like something about a table in my database. Maybe the name violates my naming convention. I simply change the name in the code.
- In the Package Manager Console in Visual Studio I create a migration named SatisfyNamingConvention. The tool will automatically generate a pathway to changing the database to match my EF Models.
Add-Migration SatisfyNamingConvention
- In the Package Manager Console, I can apply the proposed database changes.
Update-Database SatisfyNamingConvention
By default, Microsoft.AspNetCore.Identity maps its built-in IdentityUser to a database table ASPNetUser. All of the other built-in models that attend the Identity membership system have this same ASPNet prefix. I do not like the name and I prefer to use snake_case in my databases. I use the process I described above to change the default names of the tables and columns to snake case.
Single user → multi-user regime
The app was built with a single user in mind. Now I will begin to transition the app to support user-specific data. As mentioned above, this will change the way the data is structured.
First, I will want to define the new structure of the data. Under the single-user regime, the data was structured very simply. There were only two tables: ingredient and ingredient_relationship. The ingredient could either be a formulation with a parent relationship with many child ingredients, or the ingredient could be a base ingredient, with only parent relationships. Now, each ingredient_relationship will share a foreign key with a user.
User Table
Table schema will be defined mostly by Microsoft Identity API.
{{< image src="images/formulation_database_diagram_Microsoft.Identity_transparent_bg.png" caption="User Table" >}}
Further Plans
- Instead of relying entirely on manually entered nutritional data, I will replicate the foods datasets from Food Data Central in my own database as a built-in source for base ingredients. I will call these template ingredients.
- Users may use these ingredients to create user ingredients. These custom ingredients are not added to global catalog of template ingredients. I will need to decide whether to create a new table or add a flag or discriminator column for these custom ingredients.
{{< image src="images/formulation_database_diagram_all.png" caption="The Whole Thang" >}}
Done.