<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Nick Verhoeven</title>
  <id>https://www.nick-verhoeven.com/</id>
  <subtitle>Welcome to my blog! I'm Nick Verhoeven, writing about .NET software development and my personal interests, such as recipes and sustainable living.</subtitle>
  <generator uri="https://github.com/madskristensen/Miniblog.Core" version="1.0">Miniblog.Core</generator>
  <updated>2026-03-05T21:33:41Z</updated>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/zero-waste-grocery-habits/</id>
    <title>My Zero Waste Grocery Habits</title>
    <updated>2026-03-14T20:10:45Z</updated>
    <published>2026-03-05T21:33:41Z</published>
    <link href="https://www.nick-verhoeven.com/blog/zero-waste-grocery-habits/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="sustainable-living" />
    <category term="zerowaste" />
    <content type="html">&lt;p&gt;Zero waste is an ambitious term. While it is possible to reach the goal of zero package waste for some products, reducing the amount of packaging that enters my home is the best alternative for some products. Unfortunately, I often contribute to the amount of waste for many products. &lt;img class="img500" style="max-width:975px;max-height:585px;width:100%;height:100%;" src="/Image/zero_waste.webp?width=975&amp;height=585" alt="zero_waste" width="975" height="585" fetchpriority="high"&gt;&lt;br&gt;Below is a list of the things I buy to reduce or eliminate waste.&lt;/p&gt;
&lt;h2&gt;Tea&lt;/h2&gt;
&lt;p data-start="101" data-end="349"&gt;&lt;img class="img300" style="float:left;max-width:300px;max-height:257px;width:100%;height:100%;" src="/Image/zero_waste_tea.webp?width=300&amp;height=257" alt="zero_waste_tea" width="300" height="257"&gt;&lt;/p&gt;
&lt;p data-start="101" data-end="349"&gt;More than ten years ago, I started thinking about reducing packaging in my daily life. One of the first products I tried this with was tea, it felt so easy. I&amp;rsquo;ve always preferred loose leaf tea, it tastes so much better. In real tea-producing countries, people probably laugh at our tea bags because they see them as the waste left from broken or lower-quality leaves, so maybe tea bags are also a way of zero waste ;-).&lt;/p&gt;
&lt;p data-start="351" data-end="557"&gt;I buy my tea at&amp;nbsp;&lt;a title="Simon L&amp;eacute;velt" href="https://www.simonlevelt.nl" target="_blank" rel="noopener"&gt;&lt;span class="whitespace-normal"&gt;Simon L&amp;eacute;velt&lt;/span&gt;&lt;/a&gt;. You can bring your own tea tins to fill, and they even give a small discount of ten cents. It&amp;rsquo;s nice to see companies encouraging us to reduce waste.&lt;/p&gt;
&lt;p data-start="559" data-end="718"&gt;For me, this became a simple routine: bring the tin, refill it, and enjoy tea without extra packaging. It feels so normal now that I can&amp;rsquo;t imagine it anymore buying it in plastic, to fill a tin at home, and then throwing the plastic away.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-start="559" data-end="718"&gt;Deposit&amp;nbsp;jars&lt;/h2&gt;
&lt;p data-start="559" data-end="718"&gt;&lt;img class="img400" style="float:right;max-width:400px;max-height:243px;width:100%;height:100%;" src="/Image/zero_waste_disposit_jars.webp?width=400&amp;height=243" alt="zero_waste_disposit_jars" width="400" height="243"&gt;&lt;/p&gt;
&lt;p&gt;A couple of years ago, &lt;a href="https://wisselwaar.nl"&gt;Wisselwaar&lt;/a&gt; was introduced. It's perhaps the easiest way if you want to pursue a zero waste lifestyle. It's available in several product groups, such as nuts, dried fruit, cookies, coffee and tea, pasta, rice, legumes and grains. The selection within each group is limited, usually to a few items. However, the assortment grows slightly each year, which is nice. Because of this, you don&amp;rsquo;t always find exactly what you need, and some products can be expensive, so I only buy them occasionally or for special occasions.&lt;/p&gt;
&lt;h2&gt;Self‑serve refill wall&lt;/h2&gt;
&lt;p data-start="559" data-end="718"&gt;&lt;img class="img200" style="float:left;max-width:200px;max-height:170px;width:100%;height:100%;" src="/Image/zero_waste_refill.webp?width=200&amp;height=170" alt="zero_waste_refill" width="200" height="170"&gt;&lt;/p&gt;
&lt;p data-start="93" data-end="319"&gt;How I do my shopping at the refill wall is something I often get questions about at the checkout. At Ekoplaza, it&amp;rsquo;s really no different than weighing vegetables, only here you weigh products like nuts, grains, or dried fruit.&lt;/p&gt;
&lt;p data-start="321" data-end="473"&gt;I always bring my own jars and a reusable bag. I use the bag to weigh the products, then transfer everything neatly into the jars for storage at home.&lt;/p&gt;
&lt;h2 data-section-id="1izlaq8" data-start="0" data-end="26"&gt;Cleaning and toothpaste&lt;/h2&gt;
&lt;p data-start="28" data-end="215"&gt;Another category where I try to reduce packaging is cleaning products. Over time I switched to a few alternatives that use much less plastic than the typical bottles from the supermarket.&lt;/p&gt;
&lt;p data-start="28" data-end="215"&gt;&lt;img class="img300" style="float:right;max-width:300px;max-height:300px;width:100%;height:100%;" src="/Image/zero_waste_cleaning.webp?width=300&amp;height=300" alt="zero_waste_cleaning" width="300" height="300"&gt;&lt;/p&gt;
&lt;p data-start="217" data-end="471"&gt;For laundry I use for many years soap nuts and sometimes wash strips. Soap nuts are a natural product which is great. Both still have packaging but far less than the large plastic bottles of liquid detergent.&lt;/p&gt;
&lt;p data-start="473" data-end="838"&gt;The product for bathroom cleaning I use from&amp;nbsp;&lt;span class="whitespace-normal"&gt;Yokuu&lt;/span&gt;. They use bacteria-based cleaning and come as powder refills that you mix with water in a reusable spray bottle. The bottle itself is quite nice to use. It builds pressure, so it keeps spraying while pumping, which works better than many ordinary spray bottles.&lt;/p&gt;
&lt;p data-start="840" data-end="1207"&gt;Most of these alternatives are comparable in price, sometimes even cheaper. Soap nuts in particular last a long time. One downside is that the brands available at the Ekoplaza change from time to time. Products compete for shelf space, so every few years a refill system disappears and another brand replaces it, which sometimes means buying a new reusable bottle.&lt;/p&gt;
&lt;p data-start="1209" data-end="1589"&gt;For toothpaste I use toothpaste tablets. At the moment I use them from &lt;span class="whitespace-normal"&gt;Smyle&lt;/span&gt;. They are easy to use and reduce plastic tubes, although the price depends on the brand. I also noticed that the packaging slowly became larger over time. It started as a small paper bag, then a cardboard box around it, and now a bigger box to stand out on the shelf.&lt;/p&gt;
&lt;p data-start="1591" data-end="1865" data-is-last-node="" data-is-only-node=""&gt;Previously Ekoplaza also sold tablets from Denttabs, which had more tablets for the same price and less marketing packaging. I really miss them in the store, although the taste was a bit less. I&amp;rsquo;m not sure what I&amp;rsquo;ll do in the future. Ordering them online would mean a much larger shipping package and more waste, buying Smyle means accepting the growing box and higher price, or maybe deciding it&amp;rsquo;s not worth it and going back to toothpaste tubes.&lt;/p&gt;
&lt;h2 data-start="559" data-end="718"&gt;Cotton bags&lt;/h2&gt;
&lt;p data-start="144" data-end="473"&gt;For many people, it's probably already common practice to take reusable cotton bags when buying fruits and vegetables. It's super easy because they don't take up much space.&amp;nbsp;&lt;/p&gt;
&lt;h2 data-section-id="1r5g9tw" data-start="198" data-end="218"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Some habits have been easy to adopt. For example, I really like using refillable tea tins and bringing my own jars to the deposit system. However, I hope the variety of products in the deposit system grows faster in the future, making it easier to achieve zero waste.&lt;/p&gt;
&lt;p&gt;The reality is that not all products have a permanent place on the store shelves. Some products compete heavily for shelf space, and brands may increase packaging to stand out, which can make zero-waste shopping more expensive. Other products may disappear entirely, making zero-waste shopping inconsistent. It&amp;rsquo;s a long road, but small choices can still make a difference.&lt;/p&gt;
&lt;p&gt;I hope this post gives you ideas for your own shopping habits. If you have any tips or experiences to share, I'd love to hear them in the comments!&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/aspnet_identity_extending_user/</id>
    <title>Extending the ASP.NET Identity user</title>
    <updated>2026-03-17T21:46:21Z</updated>
    <published>2025-07-27T12:20:15Z</published>
    <link href="https://www.nick-verhoeven.com/blog/aspnet_identity_extending_user/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="identity" />
    <category term="miniblog" />
    <content type="html">&lt;p&gt;In the last few weeks, I wrote two blog posts:&amp;nbsp;&lt;a title="Using ASP.NET Identity" href="https://www.nick-verhoeven.com/blog/aspnet_identity/"&gt;Using ASP.NET Identity&lt;/a&gt;&amp;nbsp;and &lt;a title="Using External Login in ASP.NET Identity" href="/blog/aspnet_identity_external_login/"&gt;Using External Login in ASP.NET Identity&lt;/a&gt;. How I Added It to My MiniBlog Project" and "Part 2: Using External Login in ASP.NET Identity: How to Extend It with an External Login Provider." In this blog, I will explain how I extended the Identity user with custom fields to store additional profile information.&lt;/p&gt;
&lt;p&gt;I extended the user with a display name so that I could show who commented on a post.&lt;/p&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:750px;max-height:395px;display:block;margin-left:auto;margin-right:auto;width:100%;height:100%;" src="/Image/aspnet_identity.webp?width=750&amp;height=395" alt="aspnet_identity" width="750" height="395" fetchpriority="high"&gt;&lt;/p&gt;
&lt;h2&gt;Create a class that inherits&amp;nbsp;from IdentityUser&lt;/h2&gt;
&lt;p&gt;By default, IdentityDbContext works with the IdentityUser class to define the user table. To add custom properties, create your own class that inherits from IdentityUser. You can also add additional indexes if needed, for example:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;[Index(nameof(DisplayName), IsUnique = true)]
public class ApplicationUser : IdentityUser
{
    [MaxLength(64)]
    public required string DisplayName { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Update your DbContext&lt;/h2&gt;
&lt;p&gt;Update your DbContext. You need to configure it to use your custom user class for the Identity user table instead of the default one. For example, you can do this like this:&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;public class MiniBlogDbContext : IdentityDbContext&amp;lt;ApplicationUser&amp;gt; // specify your created user class here
{
    // your own internal DbSet(s)

    public MiniBlogDbContext(DbContextOptions&amp;lt;MiniBlogDbContext&amp;gt; options)
        : base(options)
    {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Replace IdentityUser with your custom class everywhere&lt;/h2&gt;
&lt;p&gt;Begin by updating &lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Program.cs&lt;/em&gt;&lt;/span&gt;&amp;nbsp;or&amp;nbsp;&lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Startup.cs&lt;/em&gt;&lt;/span&gt; where you register Identity. Replace IdentityUser with you custom user class.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;// Replace the IdentityUser for you custom user class
builder.Services.AddIdentity&amp;lt;ApplicationUser, IdentityRole&amp;gt;(options =&amp;gt; options.SignIn.RequireConfirmedAccount = true)
    .AddRoles&amp;lt;IdentityRole&amp;gt;() // Only needed when you use roles in your application
    .AddEntityFrameworkStores&amp;lt;DbContext&amp;gt;()
    .AddDefaultTokenProviders();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The less fun part of it is that you also need to update all the scaffolded views that use IdentityUser and replace them with your custom user class. You also need to replace IdentityUser with your custom user class in any controller or other classes that use it. For example, in the &lt;span style="text-decoration: underline;"&gt;&lt;em&gt;login.cshtml.cs&lt;/em&gt;&lt;/span&gt;:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public LoginModel(SignInManager&amp;lt;ApplicationUser&amp;gt; signInManager, ILogger&amp;lt;LoginModel&amp;gt; logger)
{
    _signInManager = signInManager;
    _logger = logger;
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-start="97" data-end="136"&gt;Don&amp;rsquo;t Forget to Update the Database&lt;/h2&gt;
&lt;p data-start="138" data-end="261"&gt;After adding your custom user&amp;nbsp;class and updating the DbContext, don&amp;rsquo;t forget to update your database schema.&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-powershell"&gt;&lt;code&gt;dotnet ef migrations add AddCustomUserFields
dotnet ef database update&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-start="204" data-end="219"&gt;Wrapping Up&lt;/h2&gt;
&lt;p data-start="221" data-end="471"&gt;This was the final part of my series on adding ASP.NET Identity to my MiniBlog project. If something isn&amp;rsquo;t working as expected, I recommend starting from the beginning with&amp;nbsp;&lt;a class="" title="Using ASP.NET Identity" href="https://www.nick-verhoeven.com/blog/aspnet_identity/" data-start="394" data-end="429"&gt;Part 1: Using ASP.NET Identity&lt;/a&gt; to ensure everything is set up correctly.&lt;/p&gt;
&lt;p data-start="473" data-end="587"&gt;Thanks for following along, I hope this series helped you integrate Identity into your own project.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/aspnet_identity_external_login/</id>
    <title>Using External Login in ASP.NET Identity</title>
    <updated>2026-03-14T20:14:41Z</updated>
    <published>2025-07-20T10:30:07Z</published>
    <link href="https://www.nick-verhoeven.com/blog/aspnet_identity_external_login/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="identity" />
    <category term="miniblog" />
    <content type="html">&lt;p&gt;Last week, I wrote this blog &lt;a title="Using ASP.NET Identity" href="/blog/aspnet_identity/"&gt;Using ASP.NET Identity&lt;/a&gt;: How I added it into my MiniBlog project. In this blog, I will explain how I added the external logins for Google and GitHub.&lt;/p&gt;
&lt;p&gt;For my blog I chose Google and GitHub because they let me create a polished, branded login experience, even as a private individual. Unlike some other providers (like Microsoft) that require you to be a registered company for verification.&lt;/p&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:750px;max-height:395px;display:block;margin-left:auto;margin-right:auto;width:100%;height:100%;" src="/Image/aspnet_identity.webp?width=750&amp;height=395" alt="aspnet_identity" width="750" height="395" fetchpriority="high"&gt;&lt;/p&gt;
&lt;h2&gt;Create an OAuth app/client at the external login provider&lt;/h2&gt;
&lt;p&gt;As mentioned above, I used Google and GitHub. You created an OAuth app/client at the following URLs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title="Create Google Authentication client" href="https://console.cloud.google.com/auth/clients" target="_blank" rel="noopener"&gt;https://console.cloud.google.com/auth/clients&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a title="Create GitHub Authentication app" href="https://github.com/settings/developers" target="_blank" rel="noopener"&gt;https://github.com/settings/developers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both pages will guide you through the necessary steps. &lt;br&gt;For Google and also some other providers that I didn't implement, you need a privacy and terms of use page.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Adding the NuGet packages&lt;/h2&gt;
&lt;p&gt;For Google and GitHub you need to install these packages below.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a title="Microsoft.AspNetCore.Authentication.Google NuGet Package" href="https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.Google" target="_blank" rel="noopener"&gt;Microsoft.AspNetCore.Authentication.Google&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a title="AspNet.Security.OAuth.GitHub NuGet Package" href="https://www.nuget.org/packages/AspNet.Security.OAuth.GitHub" target="_blank" rel="noopener"&gt;AspNet.Security.OAuth.GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're using a different provider, there's probably an official Microsoft package for it. Search for Microsoft.AspNetCore.Authentication.*. If not, the &lt;a title="AspNet.Security.OAuth.Providers Project on GitHub" href="https://github.com/aspnet-contrib/AspNet.Security.OAuth.Providers" target="_blank" rel="noopener"&gt;AspNet.Security.OAuth.Providers&lt;/a&gt; project on GitHub will most likely have one.&lt;/p&gt;
&lt;h2&gt;Configure the external providers&lt;/h2&gt;
&lt;p&gt;The only thing left to do is to add your provider to your &lt;em&gt;&lt;span style="text-decoration: underline;"&gt;Program.cs&lt;/span&gt; &lt;/em&gt;or &lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Startup.cs&lt;/em&gt;&lt;/span&gt; and implement it with the ClientId and ClientSecrets of your created OAuth app/client.&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;if (configuration["Authentication:GitHub:ClientId"] != null &amp;amp;&amp;amp; configuration["Authentication:GitHub:ClientSecret"] != null)
{
    builder.Services.AddAuthentication().AddGitHub(githubOptions =&amp;gt;
    {
        githubOptions.ClientId = configuration["Authentication:GitHub:ClientId"]!;
        githubOptions.ClientSecret = configuration["Authentication:GitHub:ClientSecret"]!;
        githubOptions.Scope.Add("user:email");
    });
}

if (configuration["Authentication:Google:ClientId"] != null &amp;amp;&amp;amp; configuration["Authentication:Google:ClientSecret"] != null)
{
    builder.Services.AddAuthentication().AddGoogle(googleOptions =&amp;gt;
    {
        googleOptions.ClientId = configuration["Authentication:Google:ClientId"]!;
        googleOptions.ClientSecret = configuration["Authentication:Google:ClientSecret"]!;
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you notice that not everything is working you might want to check out the first part: &lt;a title="Using ASP.NET Identity" href="/blog/aspnet_identity/"&gt;Using ASP.NET Identity&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-start="117" data-end="132"&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p data-start="137" data-end="400"&gt;This was the second part of adding ASP.NET Identity to support external logins to my MiniBlog project. In the &lt;a title="Extending the ASP.NET Identity" href="/blog/aspnet_identity_extending_user/"&gt;&lt;strong&gt;final part&lt;/strong&gt;&lt;/a&gt; of the series, I&amp;rsquo;ll show you how to extend the Identity user with custom fields, like a display name, so you can store and use additional profile information.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/aspnet_identity/</id>
    <title>Using ASP.NET Identity</title>
    <updated>2026-03-17T21:46:55Z</updated>
    <published>2025-07-13T19:49:34Z</published>
    <link href="https://www.nick-verhoeven.com/blog/aspnet_identity/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="identity" />
    <category term="miniblog" />
    <content type="html">&lt;p&gt;When I started customizing my MiniBlog, I wanted a more robust comment section than one that relied on JavaScript to prevent bots from adding comments. Email confirmation was an option, but I chose to require a login. While it gives me more control, unfortunately it also acts as a barrier for users trying to create an account. That's why I also wanted to support also external logins so that users wouldn&amp;rsquo;t have to create yet another account.&lt;/p&gt;
&lt;p&gt;Fortunately, there is ASP.NET Identity. I integrated it into my MiniBlog using scaffolding as a starting point. Identity provides a solid foundation for handling authentication, with built-in support for local and external logins, two-factor authentication (2FA), and more. Here are the steps I took:&lt;/p&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:750px;max-height:395px;display:block;margin-left:auto;margin-right:auto;width:100%;height:100%;" src="/Image/aspnet_identity.webp?width=750&amp;height=395" alt="aspnet_identity" width="750" height="395" fetchpriority="high"&gt;&lt;/p&gt;
&lt;h2&gt;Adding the NuGet packages&lt;/h2&gt;
&lt;p&gt;I'm using Entity Framework, so I'm also using the &lt;a title="Microsoft.AspNetCore.Identity.EntityFrameworkCore NuGet Package" href="https://www.nuget.org/packages/Microsoft.AspNetCore.Identity.EntityFrameworkCore" target="_blank" rel="noopener"&gt;Microsoft.AspNetCore.Identity.EntityFrameworkCore&lt;/a&gt; package. You can use ASP.NET Identity. If you're not using EF, then you don't need the package. However, you will need to add your implementations of some Identity interfaces, such as IUserStore and IRoleStore, among others.&lt;/p&gt;
&lt;h2&gt;Create or Update DbContext&lt;/h2&gt;
&lt;p&gt;Create or Update your DbContext to use the IdentityDbContext instead of the DbContext.&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class MiniBlogDbContext : IdentityDbContext
{
    // your own internal DbSet(s)

    public MiniBlogDbContext(DbContextOptions&amp;lt;MiniBlogDbContext&amp;gt; options)
        : base(options)
    {
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Update Program/Startup.cs&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Registrate DbContext&lt;br&gt;I use the lightest basic Azure SQL Database available. I know that it can experience transient (temporary) connectivity issues. The EnableRetryOnFailure makes it reliable in case that happens. You probably only want to use it with a cloud-based database in cases like this.
&lt;pre class="language-csharp"&gt;&lt;code&gt;builder.Services.AddbContextFactory&amp;lt;DbContext&amp;gt;(options =&amp;gt;
    options.UseSqlServer(configuration.GetConnectionString("MyConnectionString"), providerOptions =&amp;gt; providerOptions.EnableRetryOnFailure()));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Register Identity
&lt;pre class="language-csharp"&gt;&lt;code&gt;builder.Services.AddIdentity&amp;lt;IdentityUser, IdentityRole&amp;gt;(options =&amp;gt; options.SignIn.RequireConfirmedAccount = true)
    .AddRoles&amp;lt;IdentityRole&amp;gt;() // Only needed when you use roles in your application
    .AddEntityFrameworkStores&amp;lt;DbContext&amp;gt;()
    .AddDefaultTokenProviders();&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Configure cookies. You can skip this step if you prefer, as I use the default values.&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;builder.Services.ConfigureApplicationCookie(options =&amp;gt;
{
    options.Cookie.HttpOnly = true; // This ensures the authentication cookie cannot be accessed via JavaScript (for security against XSS attacks)
    options.ExpireTimeSpan = TimeSpan.FromDays(14);

    options.LoginPath = "/Identity/Account/Login";     //set the login path.  
    options.AccessDeniedPath = "/Identity/Account/AccessDenied";
    options.SlidingExpiration = true;
});&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Scaffold items&lt;/h2&gt;
&lt;p&gt;Use Add &amp;gt; New Scaffolded Item... to override the default ASP.NET Identity views. Even if you don&amp;rsquo;t plan to use all the features, it&amp;rsquo;s a good idea to scaffold all the available Identity pages. That way, you maintain full control over the UI and behavior. For any Identity pages you don't need, you can modify the logic to return a 404 Not Found response, effectively disabling those routes.&lt;/p&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:790px;max-height:599px;width:100%;height:100%;" src="/Image/aspnet_identity_scaffold_items.webp?width=790&amp;height=599" alt="aspnet_identity_scaffold_items" width="790" height="599"&gt;&lt;/p&gt;
&lt;p&gt;When you use &lt;em&gt;Manage\EnableAuthenticator.cshtml&lt;/em&gt;, be sure to add the two extra JavaScript files necessary for displaying a QR code. When you open the view, you'll see a link to the documentation.&lt;/p&gt;
&lt;h2&gt;Add a implementation of EmailSender&lt;/h2&gt;
&lt;p&gt;Of course, a login isn't complete without a email confirmation and the ability to reset your password. When you open the &lt;em&gt;Register.cshtml.cs&lt;/em&gt; or the &lt;em&gt;ForgetPassword.cshtml.cs,&lt;/em&gt; you will notice that they use an &lt;em&gt;IEmailSender&lt;/em&gt;. You only need to add an implementation. I used Microsoft's documentation for this: &lt;a title="Implementation of EmailSender for Account confirmation and password recovery" href="https://learn.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm" target="_blank" rel="noopener"&gt;https://learn.microsoft.com/en-us/aspnet/core/security/authentication/accconfirm&lt;/a&gt;. It's a simple interface with only one SendEmailAsync method, so creating your own implementation with your preferred email service should be easy.&lt;br&gt;As the Microsoft documentation describes, after adding an EmailSender you should update &lt;em&gt;RegisterConfirmation.cshtml.cs. &lt;/em&gt;Make sure to set the DisplayConfirmAccountLink to false, or even remove the code block and the EmailConfirmationUrl property entirely.&lt;/p&gt;
&lt;div class="repos-files-header-commandbar bolt-header-commandbar bolt-button-group flex-row" role="menubar"&gt;
&lt;div class="repos-files-header-commandbar bolt-header-commandbar-button-group flex-row flex-center flex-grow scroll-hidden rhythm-horizontal-8"&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;// Once you add a real email sender, you should remove this code that lets you confirm the account
DisplayConfirmAccountLink = false;
if (DisplayConfirmAccountLink)&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;I case of OutputCache&lt;/h2&gt;
&lt;p&gt;I ran into issues with the OutputCache and BasePolicy settings that I had configured. Some Identity pages were cached. This could result in a 400 Bad Request error when submitting a form for to log in, register or reset a password. This occurred because the AntiForgeryToken in the cached result was rejected. Adding a filter condition using the With method to exclude the Identity path resolves this issue. For example:&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;builder.Services.AddOutputCache(options =&amp;gt;
{
    options.AddBasePolicy(policyBuilder =&amp;gt;
    {
        policyBuilder.Expire(TimeSpan.FromSeconds(3600));

        // Disable caching for Identity paths
        policyBuilder.With(context =&amp;gt;
            !context.HttpContext.Request.Path.StartsWithSegments("/Identity", StringComparison.OrdinalIgnoreCase)
        );
    });

    options.MaximumBodySize = 262_144;
});&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-start="117" data-end="132"&gt;What&amp;rsquo;s Next&lt;/h2&gt;
&lt;p data-start="134" data-end="267"&gt;This was my first part of adding ASP.NET Identity to my MiniBlog project. In the next two blog posts, I&amp;rsquo;ll take things further:&lt;/p&gt;
&lt;ul data-start="269" data-end="546"&gt;
&lt;li data-start="269" data-end="426"&gt;
&lt;p data-start="271" data-end="426"&gt;&lt;a title="Using External Login in ASP.NET Identity" href="/blog/aspnet_identity_external_login/"&gt;&lt;strong data-start="271" data-end="282"&gt;Part 2:&lt;/strong&gt; How to add external login providers like Google or GitHub, so you can sign in with accounts you already have.&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li data-start="269" data-end="426"&gt;
&lt;p data-start="271" data-end="426"&gt;&lt;a title="Extending the ASP.NET Identity user" href="/blog/aspnet_identity_extending_user/"&gt;&lt;strong data-start="429" data-end="440"&gt;Part 3:&lt;/strong&gt; Extending the Identity user with custom fields to store additional profile information, for example the Display Name.&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/fluentassertions_or_shouldly/</id>
    <title>Using Fluent Assertions or Shouldly</title>
    <updated>2025-02-10T18:23:16Z</updated>
    <published>2025-02-05T11:53:02Z</published>
    <link href="https://www.nick-verhoeven.com/blog/fluentassertions_or_shouldly/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="unittest" />
    <category term="shouldly" />
    <category term="fluent assertions" />
    <content type="html">&lt;p&gt;After the news spread that Fluent Assertions had changed their licence in version 8, a lot of things were unclear. Now, a few weeks later, they have clarified things a bit and have also done a quick release of version 8.0.1 to address a concern that a lot of developers had and was causing a lot of frustration.&lt;/p&gt;
&lt;p&gt;Fluent Assertions version 8 is still free for non-commercial use.&amp;nbsp;version 7 is unchanged and still free for commercial use under the Apache licence.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;The licence change was frustrating because it was just a version update. If you updated all your packages, you could suddenly use the framework without a licence. After all, who expects a licence change when upgrading a free package. An extra licence check warning when using version 8.0.1 resolve this concern a bit.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;If this solves all problems and makes the community happy again, only the future will tell. Here are my thoughts on it.&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;Should you upgrade to 8?&lt;/h2&gt;
&lt;p&gt;For my blog, which I do in my spare time, it is not commercial, so I could still use Fluent Assertions 8 for free. I prefer to work with frameworks that I use or can use professionally. A licence that costs $130 per year per developer is too expensive in my opinion. It's the best assertion framework for .NET, just not worth the price.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style="text-decoration: underline;"&gt;Update 10 February 2025&lt;/span&gt;: Small businesses can now purchase a yearly licence for $50 per developer. This is only for companies with less than $1 million in revenue and a maximum of three developers. I'm unsure if this is an improvement for small businesses. If your team or revenue grows, you must update your licence.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I haven't missed any features in version 7. If you use Fluent Assertions commercially, I would recommend staying on version 7 as long as possible. If a newer version has a feature that you really miss then you could decide if its worth its price.&lt;/p&gt;
&lt;h2&gt;Lock your NuGet version&lt;/h2&gt;
&lt;p&gt;Something everybody espacially the first week advised. Lock your NuGet version so that you don't accidental upgrade without a valid license. Also on the Update tab in the NuGet Package Manager it helps so that it doesn't show the unwanted Fluent Assertions update anymore.&lt;/p&gt;
&lt;p&gt;The only thing you need to do is to edit you .csproj and add [] around the version. In that case you only allow that specific version.&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;&amp;lt;PackageReference Include="FluentAssertions" Version="[7.1.0]" /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When you have a older .NET project with still a package.config you can edit it in&lt;/p&gt;
&lt;pre class="language-markup"&gt;&lt;code&gt;&amp;lt;package id="FluentAssertions" version="7.1.0" allowedVersions="[7.1.0]" /&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/nuget/concepts/package-versioning?tabs=semver20sort#Constraining_Upgrades_To_Allowed_Versions"&gt;This Microsoft page&lt;/a&gt;&amp;nbsp;you gives you all the details and options for how to lock down a version or a range of versions.&lt;/p&gt;
&lt;h2&gt;Shouldly as alternative&lt;/h2&gt;
&lt;p&gt;&lt;img class="img500" style="float:right;max-width:429px;max-height:241px;width:100%;height:100%;" src="/Image/shouldlyorfluentassertions.webp?width=429&amp;height=241" alt="shouldlyorfluentassertions" width="429" height="241"&gt;Shouldly is an alternative with many similarities to Fluent Assertions. So should you use it? It depends on how many tests you have and which features of Fluent Assertions you use.&lt;/p&gt;
&lt;p&gt;The basics are almost the same, for example on a property level a ShouldBe() instead of a Should().Be(), and all the variants like BeNull, BeGreaterThan, BeLessThan etc.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;When testing an exception is thrown its a bit different, but Shouldly is still more then capable of doing this. The biggest difference, and whether Shouldly is the best solution for you, is how Should().BeEquivalentTo() works.&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;Exception assertion&lt;/h3&gt;
&lt;p&gt;Testing whether an exception is thrown in Shouldly is done with a Should.Throw&amp;lt;ExceptionType&amp;gt;(function), it returns the exception which you can then use to check if the message has the expected value. For example&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;// Act
var act = async () =&amp;gt; await GetTarget().DoSomething(something);

// Assert Fluent Assertions
await act.Should().ThrowExactlyAsync&amp;lt;ArgumentException&amp;gt;().WithMessage($"Doing something: {something} is not supported");

// Assert Shouldly
var exception = await Should.ThrowAsync&amp;lt;NotSupportedException&amp;gt;(act);
exception.Message.ShouldBe($"Doing something: {something} is not supported");

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It has a small advantage if you don't like the async lambda functions, or prefer to combine the call and throw assertion. This can be done like this:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;// Act &amp;amp; Assert Shouldly
var exception = await Should.ThrowAsync&amp;lt;NotSupportedException&amp;gt;(GetTarget().DoSomething(something));
exception.Message.ShouldBe($"Doing something: {something} is not supported");

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;BeEquivalentTo&lt;/h3&gt;
&lt;p&gt;Finally, the real difference in BeEquivalentTo. Shouldly is stricter and also lacks an EquivalencyAssertionOptions, at some point this are features that are really useful. For some of the examples I've made I used the User class and the Service below:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public interface IUserService
{
    void CreateUser(int code, string firstName, string lastName);
    User GetUser(int code);
}

public class User
{
    public required Guid Id { get; set; }
    public required int Code { get; set; }
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
    public required bool DbContraintField { get; set; }
    public required DateTime CreatedDate { get; set; }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the example above, I might want to do the following, which aren't possible with Shouldly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Excluding properties. The Id is a Guid. When my UserService creates a user with an Id using Guid.NewGuid(), I just want to ignore it. In Fluent Assertions I used to do it like this:&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;result.Should().BeEquivalentTo(expected, options =&amp;gt; options.Excluding(u =&amp;gt; u.Id));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;DateTime tolerance. When my UserService creates a user with a CreatedDate with a DateTime.Now() then I can predict the value, just not to the millisecond. In Fluent Assertions it is also not the easiest part, but there I did check it like:&amp;nbsp;&amp;nbsp;&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;result.Should().BeEquivalentTo(expected, options =&amp;gt; options.Using&amp;lt;DateTime&amp;gt;(ctx =&amp;gt; ctx.Subject.Should().BeCloseTo(ctx.Expectation, TimeSpan.FromSeconds(1))).WhenTypeIs&amp;lt;DateTime&amp;gt;());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;Stricter. If I am doing an integration test and my UserService creates the record in the database. I may only want to check the Code, FirstName and LastName. Excluding the fields is one option, sometimes I just want to assert against a anonymous object and only compare the properties that are in the expected object. In Fluent Assertions, I could do this like:&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;result.Should().BeEquivalentTo(new { Code = code, FirstName = firstName, LastName = lastName });&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Without strict ordering. I don't have an example for this with the User class. If I have async calls that add items to a collection, I don't know the order in which I get the collection. In Fluent Assertions I could do check it like:&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;createdUser.Should().BeEquivalentTo(expected, options =&amp;gt; options.WithoutStrictOrdering());&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Create a Extension method for Shouldly&lt;/h3&gt;
&lt;p&gt;If you use BeEquivalentTo in many places like the above, then it is probably best to stick with version 7 of Fluent Assertions. Shouldly is aware of the situation due to the licence changes of Fluent Assertions, and is considering adding some missing features. It may be easier to make the move to Shouldly in the future.&lt;/p&gt;
&lt;p&gt;Another option is to create your own extension methods to fill the gap for the features you are missing. I've created mine to help me with the 2 things I miss the most.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Excluding properties, like:&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;result.ShouldBeEquivalentTo(expected, options =&amp;gt; options.Exclude(u =&amp;gt; u.Id));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;DateTime tolerance, i now can do like:&amp;nbsp;&lt;br&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;result.ShouldBeEquivalentTo(expected, options =&amp;gt; options.UsingDateTimeTolerance(TimeSpan.FromSeconds(1)));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the future I may extend and improve it, if so I will update the code below.&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;[ShouldlyMethods]
public static class ShouldlyExtensions
{
    public static void ShouldBeEquivalentTo&amp;lt;T&amp;gt;(this T? actual, T? expected, Action&amp;lt;EquivalencyOptions&amp;lt;T&amp;gt;&amp;gt; options) where T : class
    {
        if (typeof(IEnumerable).IsAssignableFrom(typeof(T)) &amp;amp;&amp;amp; typeof(T) != typeof(string))
        {
            throw new InvalidOperationException("This overload is not intended for collection types.");
        }

        var equivalencyOptions = ValidateAndSetupOptions(actual, expected, options);

        CompareObjects(actual, expected, equivalencyOptions, new HashSet&amp;lt;(object, object)&amp;gt;());
    }

    public static void ShouldBeEquivalentTo&amp;lt;T&amp;gt;(this IEnumerable&amp;lt;T?&amp;gt; actual, IEnumerable&amp;lt;T?&amp;gt; expected, Action&amp;lt;EquivalencyOptions&amp;lt;T&amp;gt;&amp;gt; options)
    {
        var equivalencyOptions = ValidateAndSetupOptions(actual, expected, options);
        var actualList = actual.ToList();
        var expectedList = expected.ToList();

        actualList.Count.ShouldBe(expectedList.Count, "Collection counts do not match.");

        for (int i = 0; i &amp;lt; actualList.Count; i++)
        {
            CompareObjects(actualList[i], expectedList[i], equivalencyOptions, new HashSet&amp;lt;(object, object)&amp;gt;());
        }
    }

    private static EquivalencyOptions&amp;lt;T&amp;gt; ValidateAndSetupOptions&amp;lt;T&amp;gt;(object? actual, object? expected, Action&amp;lt;EquivalencyOptions&amp;lt;T&amp;gt;&amp;gt; options)
    {
        if (actual == null &amp;amp;&amp;amp; expected != null || actual != null &amp;amp;&amp;amp; expected == null)
        {
            throw new ShouldAssertException(@$"Comparing object equivalence:
actual: {actual?.GetType().ToString() ?? "null"}
expected: {expected?.GetType().ToString() ?? "null"}");
        }

        var equivalencyOptions = new EquivalencyOptions&amp;lt;T&amp;gt;();
        options(equivalencyOptions);

        return equivalencyOptions;
    }

    private static void CompareObjects&amp;lt;T&amp;gt;(object? actual, object? expected, EquivalencyOptions&amp;lt;T&amp;gt; options, HashSet&amp;lt;(object, object)&amp;gt; visitedObjects, string parentPath = "")
    {
        if (actual == null &amp;amp;&amp;amp; expected == null)
        {
            return;
        }

        if (actual == null || expected == null)
        {
            throw new ShouldAssertException(@$"Comparing object equivalence, at path '{parentPath}':
actual: {actual?.GetType().ToString() ?? "null"}
expected: {expected?.GetType().ToString() ?? "null"}");
        };

        if (visitedObjects.Contains((actual, expected)))
        {
            return;
        }
        visitedObjects.Add((actual, expected));

        var properties = actual.GetType().GetProperties();

        foreach (var property in properties)
        {
            var propertyPath = string.IsNullOrEmpty(parentPath) ? property.Name : $"{parentPath}.{property.Name}";

            if (options.ExcludedProperties.Contains(propertyPath) || property.GetIndexParameters().Length &amp;gt; 0)
            {
                continue;
            }

            ComparePropertyValues(property.GetValue(actual), property.GetValue(expected), property, options, visitedObjects, propertyPath);
        }
    }

    private static void ComparePropertyValues&amp;lt;T&amp;gt;(object? actualValue, object? expectedValue, System.Reflection.PropertyInfo property, EquivalencyOptions&amp;lt;T&amp;gt; options, HashSet&amp;lt;(object, object)&amp;gt; visitedObjects, string propertyPath)
    {
        var hasNull = actualValue == null || expectedValue == null;
        if (!hasNull &amp;amp;&amp;amp; property.PropertyType == typeof(DateTime) &amp;amp;&amp;amp; options.DateTimeTolerance.HasValue)
        {
            ((DateTime)actualValue!).ShouldBe((DateTime)expectedValue!, options.DateTimeTolerance.Value, $"Property {propertyPath} does not match.");
        }
        else if (!hasNull &amp;amp;&amp;amp; property.PropertyType == typeof(DateTimeOffset) &amp;amp;&amp;amp; options.DateTimeTolerance.HasValue)
        {
            ((DateTimeOffset)actualValue!).ShouldBe((DateTimeOffset)expectedValue!, options.DateTimeTolerance.Value, $"Property {propertyPath} does not match.");
        }
        else if (!hasNull &amp;amp;&amp;amp; IsCollectionType(property.PropertyType))
        {
            CompareCollections(actualValue!, expectedValue!, options, visitedObjects, propertyPath);
        }
        else if (!hasNull &amp;amp;&amp;amp; IsComplexType(actualValue!))
        {
            CompareObjects(actualValue, expectedValue, options, visitedObjects, propertyPath);
        }
        else
        {
            actualValue.ShouldBe(expectedValue, $"Property {propertyPath} does not match.");
        }
    }

    private static void CompareCollections&amp;lt;T&amp;gt;(object actual, object expected, EquivalencyOptions&amp;lt;T&amp;gt; options, HashSet&amp;lt;(object, object)&amp;gt; visitedObjects, string parentPath = "")
    {
        var actualList = ((IEnumerable&amp;lt;object&amp;gt;)actual).ToList();
        var expectedList = ((IEnumerable&amp;lt;object&amp;gt;)expected).ToList();

        actualList.Count.ShouldBe(expectedList.Count, "Collection counts do not match.");

        for (int i = 0; i &amp;lt; actualList.Count; i++)
        {
            CompareObjects(actualList[i], expectedList[i], options, visitedObjects, parentPath);
        }
    }

    private static bool IsCollectionType(Type type)
    {
        return typeof(IEnumerable).IsAssignableFrom(type) &amp;amp;&amp;amp; type != typeof(string);
    }

    private static bool IsComplexType(object obj)
    {
        var type = obj.GetType();
        return !type.IsPrimitive
            &amp;amp;&amp;amp; !type.IsEnum
            &amp;amp;&amp;amp; type != typeof(string)
            &amp;amp;&amp;amp; type != typeof(decimal)
            &amp;amp;&amp;amp; type != typeof(DateTime)
            &amp;amp;&amp;amp; type != typeof(DateTimeOffset);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;public class EquivalencyOptions&amp;lt;T&amp;gt;
{
    public HashSet&amp;lt;string&amp;gt; ExcludedProperties { get; } = new HashSet&amp;lt;string&amp;gt;();
    public TimeSpan? DateTimeTolerance { get; private set; }

    public EquivalencyOptions&amp;lt;T&amp;gt; Exclude(Expression&amp;lt;Func&amp;lt;T, object&amp;gt;&amp;gt; propertyExpression)
    {
        ArgumentNullException.ThrowIfNull(propertyExpression);

        ExcludedProperties.Add(GetPropertyPath(propertyExpression));
        return this;
    }

    public EquivalencyOptions&amp;lt;T&amp;gt; UsingDateTimeTolerance(TimeSpan tolerance)
    {
        DateTimeTolerance = tolerance;
        return this;
    }

    private string GetPropertyPath(Expression expression)
    {
        var propertyNames = new List&amp;lt;string&amp;gt;();
        ExtractPropertyPath(expression, propertyNames);
        propertyNames.Reverse();
        return string.Join(".", propertyNames);
    }

    private void ExtractPropertyPath(Expression expression, List&amp;lt;string&amp;gt; propertyNames)
    {
        switch (expression)
        {
            case MemberExpression memberExpression:
                propertyNames.Add(memberExpression.Member.Name);
                ExtractPropertyPath(memberExpression.Expression!, propertyNames);
                break;
            case MethodCallExpression methodCallExpression:
                foreach (var argument in methodCallExpression.Arguments.Reverse())
                {
                    ExtractPropertyPath(argument, propertyNames);
                }
                break;
            case UnaryExpression unaryExpression:
                ExtractPropertyPath(unaryExpression.Operand, propertyNames);
                break;
            case LambdaExpression lambdaExpression:
                ExtractPropertyPath(lambdaExpression.Body, propertyNames);
                break;
            case ParameterExpression:
                break;
            default:
                throw new ArgumentException("Invalid property expression");
        }
    }
}&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/container_hosting_https/</id>
    <title>Hosting a container with HTTPS</title>
    <updated>2026-03-14T20:15:03Z</updated>
    <published>2024-11-27T13:35:25Z</published>
    <link href="https://www.nick-verhoeven.com/blog/container_hosting_https/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="container" />
    <category term="docker" />
    <content type="html">&lt;p&gt;Running a .NET web app locally inside a container using Docker is super easy. Visual Studio does almost everything for you. It is not much more than right-clicking on your project and under &lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Add&lt;/em&gt;&lt;/span&gt; selecting &lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Docker Support&lt;/em&gt;&lt;/span&gt;. You get a&amp;nbsp;&lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Dockerfile&lt;/em&gt;&lt;/span&gt; that is ready to run. By default a self-signed certificate is added to use HTTPS in Docker locally, the only thing you need to do is accept the self-signed certificate.&lt;/p&gt;
&lt;p&gt;Deploying to Azure is also easy, you can manage the certificates in your Web App and even use a free certificate. The only catch is that Azure handles the HTTPS, but your container doesn't have the certificate, it's handled by a proxy. Azure calls the container from the proxy using HTTP.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:1000px;max-height:528px;width:100%;height:100%;" src="/Image/cloud_azere_ssl_docker.webp?width=1000&amp;height=528" alt="cloud_azere_ssl_docker" width="1000" height="528" fetchpriority="high"&gt;&lt;/p&gt;
&lt;p&gt;The only issue you may encounter is that your container doesn't know that the request was made over HTTPS. You may notice this if you use, as in my case, a:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Request.Scheme&lt;/li&gt;
&lt;li&gt;services.AddHsts (for the &lt;span class="treeLabel stringLabel" data-level="1"&gt;strict-transport-security header)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class="treeLabel stringLabel" data-level="1"&gt;An AddProgressiveWebApp nuget such as &lt;a href="https://www.nuget.org/packages/WebEssentials.AspNetCore.PWA/"&gt;WebEssentials.AspNetCore.PWA&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class="treeLabel stringLabel" data-level="1"&gt;External OAuth login provider&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span class="treeLabel stringLabel" data-level="1"&gt;All of these won't start or start using HTTP, which we don't want.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span class="treeLabel stringLabel" data-level="1"&gt;The solution for this is super simple. Add the following to your&amp;nbsp;&lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Program.cs&lt;/em&gt;&lt;/span&gt; or &lt;span style="text-decoration: underline;"&gt;&lt;em&gt;Startup.cs&lt;/em&gt;&lt;/span&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedProto
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It will pass the value of the original request scheme, HTTP or HTTPS, to your app inside the container.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;When I updated my web app to use Sidecar support, I found that it wasn't enough. For this reason, I now use:&lt;/p&gt;
&lt;pre class="language-csharp"&gt;&lt;code&gt;app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
});

app.Use(async (context, next) =&amp;gt;
{
    if (context.Request.Headers.TryGetValue("X-Forwarded-Proto", out var proto) &amp;amp;&amp;amp;
        proto.ToString().Equals("https", StringComparison.OrdinalIgnoreCase))
    {
        context.Request.Scheme = "https";
    }

    await next();
});&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/upgradedotnet9/</id>
    <title>Upgrade to .NET 9</title>
    <updated>2026-03-14T20:15:15Z</updated>
    <published>2024-11-14T20:58:36Z</published>
    <link href="https://www.nick-verhoeven.com/blog/upgradedotnet9/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="miniblog" />
    <content type="html">&lt;p&gt;Just a few days ago .NET 9 was released. I was actually doubted to deploy my blog later and use .NET 9 directly. This time I took the safe way and did it in 2 steps.&amp;nbsp;&lt;br&gt;I wanted to upgrade even though it is a standard term support instead of a long term support. Mainly because I try to stay on the latest version.&lt;/p&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:1000px;max-height:516px;width:100%;height:100%;" src="/Image/dotnet9.webp?width=1000&amp;height=516" alt="dotnet9" width="1000" height="516" fetchpriority="high"&gt;&lt;/p&gt;
&lt;p&gt;.NET 9 isn't a release with the biggest new features. There are some like System.Text.Json that got some extra features, not that important for my blog project. Also Entity Framework has some nice features, but even more performance improvements. That is actually the biggest difference in .NET 9 at some point they really made a big performance improvement which is nice.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/oatmilk/</id>
    <title>Oat milk</title>
    <updated>2026-03-14T20:15:25Z</updated>
    <published>2024-11-13T16:43:57Z</published>
    <link href="https://www.nick-verhoeven.com/blog/oatmilk/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="recipes" />
    <category term="sustainable-living" />
    <category term="oat milk" />
    <category term="plasticfree" />
    <category term="zerowaste" />
    <content type="html">&lt;h2&gt;Make your own oat milk&lt;/h2&gt;
&lt;p&gt;&lt;img class="img500" style="float:left;max-width:500px;max-height:300px;width:100%;height:100%;" src="/Image/cup_of_oat_milk.webp?width=500&amp;height=300" alt="cup_of_oat_milk" width="500" height="300" is="" fetchpriority="high"&gt;&lt;/p&gt;
&lt;p&gt;My personal favourite plant-based milk is oat milk. It has a nice creamy texture and a mild, slightly sweet flavour. Oats are also an environmentally friendly crop. It doesn't need much water and can be grown in many places, even locally and in the relatively colder Netherlands.&lt;/p&gt;
&lt;p&gt;For years I took the easy way out and just bought oat milk in the shop. A few years ago that all prices went up by 10% or more, even for oat milk. Which was a bit odd because almost 90% of it is water. At the same time, it became impossible to buy organic oat milk with added calcium after the European Court of Justice ruled that lithothamnium couldn't be used as an additive to enrich it with calcium.&lt;/p&gt;
&lt;p&gt;So finally I decided to make my own oat milk instead of buying non-organic oat milk or organic oat milk without calcium. It also saves a lot of waste, no more empty drinks cartons.&lt;/p&gt;
&lt;h2&gt;Ingredients&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 litre of cold water&lt;/li&gt;
&lt;li&gt;120 grams of oats&lt;/li&gt;
&lt;li&gt;4 grams lithothamnium&lt;/li&gt;
&lt;li&gt;1 teaspoon of oil&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you don't want to add extra calcium, you can leave out the lithothamnium. You can also leave out the oil if you prefer, but this will make the milk less creamy. The oil I use is Omage 3-6-9 Oil from EkoPlaza, which is a blend of beetroot oil, sunflower oil, linseed oil, evening primrose oil and tocopherol. You can use a different oil, but it is best to use an oil with a neutral taste.&lt;/p&gt;
&lt;h2&gt;Steps&lt;/h2&gt;
&lt;p&gt;&lt;img class="img300" style="max-width:287px;max-height:383px;float:right;width:100%;height:100%;" src="/Image/make_oatmilk.webp?width=287&amp;height=383" alt="Make oatmilk" width="287" height="383"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prepare a bowl with a cloth for straining the oat milk. I use 2 cheesecloth cloths and moisten them beforehand so that they filter better.&lt;/li&gt;
&lt;li&gt;Put the oats and water in the blender. Often, especially in the summer when it is hot, I add a few ice cubes. I use a kitchen scale to check that I have added 1 litre.&lt;/li&gt;
&lt;li&gt;Blend the ingredients for 30/35 seconds on the highest setting of your blender. A high powered blender is recommended, I use a Vitamix A3500. If you think your blender is not powerful enough, you can increase the blend time. In that case you may also want to add extra ice cubes to keep the oat milk cold, as it will turn to oat porridge or it gets slimy as it heats up.&lt;/li&gt;
&lt;li&gt;Pour the oat milk into the bowl with the cloths&lt;/li&gt;
&lt;li&gt;Gently squeeze the oat milk through the cloth. If you squeeze too hard you will find that it get messy and you will get a less smooth result. Start by squeezing gently with a few fingers, like a stress ball, so that you don't put too much pressure on the cloth. When almost all the moisture is out, you can squeeze a little harder.&lt;br&gt;Repeat for the second cloth, this is always quick and easy because 99% of the milk has been filtered through the first cloth.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want oat milk with as few ingredients as possible, you are done.&lt;/p&gt;
&lt;h2&gt;Extra steps&lt;/h2&gt;
&lt;p&gt;&lt;img class="img250" style="float:right;max-width:245px;max-height:327px;width:100%;height:100%;" src="/Image/bottle_of_oatmilk.webp?width=245&amp;height=327" alt="Bottle of oatmilk" width="245" height="327"&gt;&lt;/p&gt;
&lt;p&gt;If, like me, you want your oat milk a little creamier and with added calcium, there are a few extra steps.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Quickly rinse the blender with water&lt;/li&gt;
&lt;li&gt;Pour the oat milk back into the blender&lt;/li&gt;
&lt;li&gt;Add 4 grams of lithothamnium.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Add 1 teaspoon of oil&lt;/li&gt;
&lt;li&gt;Blend again on medium speed for +/- 5 to 10 seconds. I usually use speed 6 or 6+ (my blender goes up to 10).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can now pour your oat milk into a bottle and enjoy your fresh glass of oat milk. I drink 1 glass a day, you can leave it in the fridge for a day or 4.&lt;/p&gt;
&lt;h2&gt;Waste?&lt;/h2&gt;
&lt;p&gt;What do you do with the filter pulp? Throwing it away would be a real waste. You can use it in different products. I almost always use it in my bread, instead of 500 grams of flour I use 450 + the pulp. It also affects the amount of water needed for the bread, I use between 15 and 20% less water, depending on the type of flour. You can also use it for all kinds of other baking recipes like cookies or pancakes.&lt;/p&gt;</content>
  </entry>
  <entry>
    <id>https://www.nick-verhoeven.com/blog/blog-runs-on/</id>
    <title>What does my blog run on?</title>
    <updated>2026-03-14T20:15:41Z</updated>
    <published>2024-11-13T16:43:43Z</published>
    <link href="https://www.nick-verhoeven.com/blog/blog-runs-on/" />
    <author>
      <name>test@example.com</name>
      <email>Nick Verhoeven</email>
    </author>
    <category term="dev" />
    <category term="miniblog" />
    <content type="html">&lt;p&gt;&lt;img class="img500" style="float:right;max-width:500px;max-height:300px;width:100%;height:100%;" src="/Image/blog_run_on.webp?width=500&amp;height=300" alt="blog_run_on" width="500" height="300" fetchpriority="high"&gt;&lt;/p&gt;
&lt;h2&gt;The basis&lt;/h2&gt;
&lt;p&gt;As a starting point for my blog I used &lt;a href="https://github.com/madskristensen/Miniblog.Core" target="_blank" rel="noopener"&gt;Mads Kristensen MiniBlog&lt;/a&gt;. It is a nice little C# .NET blog project with all the important things in it. At the moment I'm using it, .NET 8 has already been released the first thing I did was upgrade it, which was quite easy. I also configured it to run in a container, which is helpful when developing with Visual Studio on Windows and debugging and testing with Docker Desktop on Linux.&lt;/p&gt;
&lt;h2&gt;Things I've removed&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The Azure.ImageOptimizer package. I've replaced it with a logic that, when saving a blog, checks the images used in the HTML and makes sure there is an optimised image for it. I'll do a blog about this in the future, I just have some ideas on the roadmap to improve it a bit more.&lt;/li&gt;
&lt;li&gt;The WilderMinds.MetaWeblog and therefore the MetaWeblogService. It is my personal blog and I plan only to maintain it only from the site itself. To keep it smaller and simpler I've removed the MetaWeblog API implementation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Styling&lt;/h2&gt;
&lt;p&gt;Default SCSS was already used for styling, I've updated the stylesheets to my personal taste. By default it also includes the &lt;a href="https://github.com/ligershark/WebOptimizer" target="_blank" rel="noopener"&gt;LigerShark.WebOptimizer&lt;/a&gt; and &lt;a href="https://github.com/ligershark/WebOptimizer.Sass" target="_blank" rel="noopener"&gt;LigerShark.WebOptimizer.Sass&lt;/a&gt; libraries for bundling and minifying CSS and JavaScript. The Sass library adds a SCSS to the CSS compiler, making using SCSS just as easy as using CSS.&lt;/p&gt;
&lt;h2&gt;Scripts&lt;/h2&gt;
&lt;p&gt;By default it doesn't use any JavaScript frameworks such as Angular, React, Vue and even old school JQuery. This saves a tiny bit of time loading and parsing a large javascript library before rendering the page. When I've added the AspNetCore.Identity for the logins I've got on these default views also JQuery, in the near future I'll fix it so that JQuery isn't needed anymore. There are a few custom JavaScripts in the project, they are only a few kb's big. There are also a few specific libraries are used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.tiny.cloud/" target="_blank" rel="noopener"&gt;TinyMCE&lt;/a&gt;, the WYSIWYG that is used to write the blogs. By default it was using an older version 4.8, I've updated it to a more recent version 7.4. I also customised the add image button. It now has a popup where I can upload, browse and select images.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prismjs.com/" target="_blank" rel="noopener"&gt;PrismJS&lt;/a&gt;, when you notice a block of source code in a blog, PrismJS &lt;span style="font-family: Questrial, sans-serif; font-size: 16px;"&gt;highlights &lt;/span&gt;the syntax. It's a standard TinyMCE plugin, it was also using an older version 1.15, I've updated it to version 1.29. Also added SQL and PowerShell to the options that can be selected from within TinyMCE.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fengyuanchen.github.io/cropperjs/" target="_blank" rel="noopener"&gt;Cropper&lt;/a&gt;, when you create a login you can add a profile picture. Cropper is used to edit your profile picture. Creating an account wasn't possible in the default version. Maintaining a profile picture wasn't possible either, gravatar.com was used instead. I chose to require a login because it is an easier way to block bot comments, and limit the amount of emails to verify the user/comment. I could still use gravatar.com, but using Cropper was more fun for me.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Libraries&lt;/h2&gt;
&lt;p&gt;A lot of standard .NET libraries of course, its so boring to mention those. A few of the libraries I added extra are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/zzzprojects/html-agility-pack" target="_blank" rel="noopener"&gt;Html Agility Pack&lt;/a&gt;, I use it when saving a blog&amp;nbsp;to update the img tags in the HTML. For example, TinyMCE applied a width and height attribute after scaling the image, then the page wasn't resposive anymore. Now it changes it to a max-width and max-height and applies a CSS class that is used to override the float behavioir with a media query. It also updateds the url to fetch an optimised image for the correct dimensions.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Microsoft.EntityFrameworkCore, not really special and worth mentioning. By default the MiniBlog uses just XML on the disk. I decided to use a database and used EF as the ORM. Its not really my preferred ORM, I like SQL and like having more control over the queries. For a small blog EF generates queries that are more then fine, also nice to keep my knowledge of EF a bit up to date.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/saucecontrol/PhotoSauce" target="_blank" rel="noopener"&gt;PhotoSauce.MagicScaler&lt;/a&gt; is used to generate the optimised images. Because my project runs on Linux, I'm using prereleased versions for the supported file types. I like this library because of the performance and the memory consumption. I don't mind the risk of using a prerelease version, I haven't encountered any problems, if I do I will figure it out or work around it.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ZiggyCreatures/FusionCache" target="_blank" rel="noopener"&gt;ZiggyCreatures.FusionCache&lt;/a&gt; after seeing the video on the .NET YouTube channel this year I was sold. Things like the protection for cache stampede, fail-safe and more control over timeout makes it a better choich then using standard MemoryCache. It even has a lot of extra nice features that I dont' use.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What is it hosting on?&lt;/h2&gt;
&lt;p&gt;&lt;img class="img500" style="max-width:1000px;max-height:368px;width:100%;height:100%;" src="/Image/cloud_image.webp?width=1000&amp;height=368" alt="cloud_image" width="1000" height="368"&gt;&lt;/p&gt;
&lt;p&gt;Its running in Azure and using these services, I use a lowest possible specs to save the costs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure Web App for Containers in a Linux Basic B1 App Service plan.&amp;nbsp;&amp;nbsp;&lt;/li&gt;
&lt;li&gt;Azure SQL Database its a single database on the Basic tier&lt;/li&gt;
&lt;li&gt;Azure Blob Storage in LRS on the Hot Access tier&lt;/li&gt;
&lt;li&gt;Grafana Cloud to monitor for errors and see if the performance isn't that terrible while using the lowest possible specs.&lt;/li&gt;
&lt;/ul&gt;</content>
  </entry></feed>