Generated by All in One SEO Pro v4.9.4.2, this is an llms-full.txt file, used by LLMs to index the site. # PureSourceCode All technologies, only pure source code ## Posts ### [Complete Rust programming Guide for Beginners](https://puresourcecode.com/programming-languages/rust/complete-rust-programming-guide-for-beginners/) **Published:** February 19, 2026 **Author:** Enrico **Excerpt:** Learn Rust programming from basics to advanced concepts: ownership, functions, data types, control flow, error handling, and collections. **Content:** [Rust](https://puresourcecode.com/tag/rust) is a modern systems programming language designed for speed, safety, concurrency, and portability, making it a favorite among developers worldwide. Created by Graydon Hoare in 2007 and sponsored by Mozilla, Rust’s first stable release was in 2014. It shares similarities with C and C++ but introduces innovative concepts like ownership to ensure memory safety without a garbage collector. According to the Stack Overflow Developer Survey, Rust has been the most admired programming language for seven consecutive years, with more than 80% of developers expressing a desire to continue using it. Major tech companies such as Dropbox, Discord, Amazon, Facebook, Microsoft, and Mozilla rely on Rust for building reliable and efficient software. ### Why Choose Rust as a programming language? Rust strikes a unique balance between: - **Speed:** Comparable to C and C++ due to its compiled nature. - **Safety:** Guarantees memory safety via ownership rules, preventing bugs common in C/C++. - **Concurrency:** Supports safe multi-threaded programming without data races. - **Portability:** Rust programs can be compiled once and run across Windows, Linux, and macOS. Notably, the White House has recommended transitioning from C and C++ to Rust to enhance cybersecurity through memory-safe programming. ## Getting Started with Rust ### Installing Rust Visit [rust-lang.org](https://rust-lang.org/) to download and install Rust easily via `rustup`, the official Rust installer and version manager. You can install Rust on Windows, Linux, or macOS using simple commands or executables. After installation, verify by running: ``` rustc --version cargo --version ``` These commands check the Rust compiler and Cargo package manager versions. ### Writing Your First Rust Program Create a Rust file named `hello.rs`: fn main() { println!(“Hello, world!”); } Compile and run it using: rustc hello.rs ./hello Alternatively, use Cargo, Rust’s package manager and build tool: cargo new hello_project cd hello_project cargo run Cargo automatically handles compilation and running, streamlining development. ## Core Concepts in Rust Programming ### Primitive Data Types Rust is a statically typed language, requiring explicit or inferred data types for variables. Primitive types include: - **Integers:** Signed (`i8`, `i16`, `i32`, `i64`, `i128`) and unsigned (`u8`, `u16`, `u32`, `u64`, `u128`). The number indicates the bit size and memory consumed. - **Floating-Point:** `f32` and `f64` for decimal numbers. - **Boolean:** `bool` values are `true` or `false`. - **Character:** `char` represents a single Unicode scalar value. Example: let x: i32 = 42; // signed 32-bit integer let y: u64 = 100; // unsigned 64-bit integer let pi: f64 = 3.14; // 64-bit float let is_snowing: bool = true; let letter: char = ‘A’; Rust enforces strict type safety; for example, assigning negative values to unsigned integers causes compile-time errors. ### Compound Data Types Rust supports grouping multiple values: - **Arrays:** Fixed-size, homogeneous collections. let numbers: [i32; 5] = [1, 2, 3, 4, 5]; - **Tuples:** Fixed-size, heterogeneous collections. let human: (&str, i32, bool) = (“Alice”, 30, false); - **Slices:** Dynamically-sized views into arrays or strings, represented as references with `&`. - **Strings:** Two main types in Rust: - **String slice (`&str`):** Immutable reference to a string literal stored in binary or elsewhere. - **String (`String`):** Growable, mutable, heap-allocated string type. Example of mutable strings: let mut s = String::from(“Hello”); s.push_str(” World”); ## Functions in Rust Functions are declared with the `fn` keyword, follow snake\_case naming, and can accept parameters and return values. Example: fn main() { hello_world(); tell_height(182); } fn hello_world() { println!(“Hello, Rust!”); } fn tell_height(height: i32) { println!(“My height is {} cm”, height); } - **Return values:** Functions return values via expressions without semicolons. ``` fn add(a: i32, b: i32) -> i32 { a + b // returns the sum } ``` - **Expressions vs Statements:** Expressions return values; statements do not. ## Ownership, Borrowing, and References ### Understanding Ownership Rust’s ownership system manages memory without a garbage collector. The rules are: 1. Each value has a single owner. 2. Only one owner at a time. 3. When the owner goes out of scope, the value is dropped. Example: let s1 = String::from(“rust”); let s2 = s1; // ownership moved to s2; s1 is no longer valid ### Borrowing and References You can borrow values without taking ownership by using references: - **Immutable references:** Multiple allowed; read-only access. let s = String::from(“hello”); let len = calculate_length(&s); fn calculate_length(s: &String) -> usize { s.len() } - **Mutable references:** Only one allowed; allows modifying the borrowed value. let mut x = 5; let r = &mut x; *r += 1; Rust enforces rules at compile time to prevent data races or dangling pointers. ## Variables, Mutability, and Shadowing - Variables are **immutable by default**. To mutate, use `mut`: let mut a = 5; a = 10; // allowed because of mut - **Constants:** Declared with `const`, always immutable, must have type annotation, and can be declared in global scope: ``` const PI: f64 = 3.14; ``` - **Shadowing:** Allows redeclaring a variable with the same name, optionally changing its type or value. ``` let x = 5; let x = x + 1; // shadows previous x ``` Shadowing differs from mutability as it creates a new binding. ## Control Flow in Rust ### If Expressions Control flow based on conditionals: ``` let age = 18; if age >= 18 { println!("You can drive"); } else { println!("You cannot drive"); } ``` Supports `else if` chains and conditional assignment: ``` let number = if condition { 5 } else { 6 }; ``` ### Loops Rust offers: - **loop:** Infinite loop until `break`. ``` loop { println!("Hello"); break; } ``` - **while:** Loop while a condition is true. ``` while number != 0 { println!("{}", number); number -= 1; } ``` - **for:** Loop over collections. ``` for i in 1..5 { println!("{}", i); } ``` - **Loop labels:** Used in nested loops for clarity with `break` and `continue`. ## Structs and Enums ### Structs Custom data types grouping related fields, with named fields: ``` struct User { active: bool, username: String, email: String, sign_in_count: u64, } let mut user1 = User { active: true, username: String::from("user123"), email: String::from("user@example.com"), sign_in_count: 1, }; user1.email = String::from("new_email@example.com"); ``` - **Tuple structs:** Like tuples with named types but unnamed fields. - **Unit-like structs:** Empty structs useful for traits. ### Enums Enums define types that can be one of several variants, optionally storing data: ``` enum IpAddrKind { V4, V6, } let four = IpAddrKind::V4; let six = IpAddrKind::V6; ``` Variants can hold data: ``` enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); ``` Enhanced enums allow more complex data representation. --- ## Error Handling in Rust Rust encourages explicit error handling through: ### Option Used when a value might be present or absent, avoiding null references: ``` fn divide(numerator: f64, denominator: f64) -> Option { if denominator == 0.0 { None } else { Some(numerator / denominator) } } ``` ### Result Used for recoverable errors with detailed error information: ``` fn divide(numerator: f64, denominator: f64) -> Result { if denominator == 0.0 { Err(String::from("Cannot divide by zero")) } else { Ok(numerator / denominator) } } ``` Use `match` to handle returned `Option` or `Result`. --- ## Common Collections in Rust ### Vectors Growable arrays storing homogeneous data: ``` let mut v: Vec = Vec::new(); v.push(1); v.push(2); ``` Access elements by indexing or the `get` method, which returns an `Option`. ### UTF-8 Strings Rust strings are UTF-8 encoded, allowing multilingual text: ``` let mut s = String::from("Hello"); s.push('!'); s.push_str(" World"); ``` ### HashMaps Key-value stores similar to dictionaries in other languages: ``` use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); } ``` Rust uses secure hashing algorithms to protect against denial-of-service attacks. --- ## Conclusion This comprehensive Rust course has covered fundamental topics crucial for beginners to start programming effectively: - Installation and setup - Data types: primitive and compound - Functions and control flow - Ownership, borrowing, and references for memory safety - Variables, mutability, and shadowing - Structs and enums for custom data types - Error handling with Option and Result - Collections: vectors, strings, and hashmaps Rust offers the power of low-level programming with the safety and productivity of modern languages. With its growing popularity and industry adoption, mastering Rust opens doors to creating fast, reliable, and concurrent applications. Keep practicing these concepts, explore Rust’s official documentation, and build projects to deepen your understanding. Happy coding with Rust! ## Common error That error is a classic Windows‑MSVC toolchain issue: Rust is trying to link your program using **link.exe**, but it can’t find **msvcrt.lib** — the Microsoft C Runtime library. On Windows, Rust **requires** the MSVC build tools (even if you don’t use Visual Studio itself). When this library is missing, it usually means one of three things: Why this happens **1. The MSVC toolchain is installed incorrectly or incompletely** Rust expects to find: ``` C:\Program Files\Microsoft Visual Studio\\Community\VC\Tools\MSVC\\lib\x64\msvcrt.lib ``` Your error shows: ``` LINK : fatal error LNK1104: cannot open file 'msvcrt.lib' ``` This means the file is missing or the installation is corrupted. **2. You installed Visual Studio but not the “Desktop development with C++” workload** This workload contains the MSVC linker, Windows SDK, and the CRT libraries. **3. You installed Visual Studio 2022 *Preview* or *Build Tools* only** Rust sometimes fails to detect incomplete installations. ### How to fix it (the reliable way) Open **Visual Studio Installer** → Modify your installation → ensure these are checked: ✔ Desktop development with C++ Inside it, make sure these components are selected: - MSVC v143 or v142 build tools - Windows 10/11 SDK - C++ CMake tools - C++ ATL/MFC (optional) - C++ core features This guarantees `msvcrt.lib` is installed. **Categories:** Rust **Tags:** rust, Rust data types, Rust ownership, Rust programming, Rust tutorial **Hashtags:** rust --- ### [Create a list of unavailable emails](https://puresourcecode.com/tools/create-a-list-of-unavailable-emails/) **Published:** December 17, 2025 **Author:** Enrico **Excerpt:** I publish the Visual Basic (VBA) code for Microsoft Outlook. It is used to create a list of unavailable emails. **Content:** In this post, I publish the Visual Basic (VBA) code for Microsoft Outlook. It is used to create a list of unavailable emails. For example, I want to personalize an email to send to all my colleagues. Each email has a different link. For this reason, I use Microsoft Word to send the emails using the **Mailing** function. ## Mail Merge in Microsoft Word Mail Merge is one of Microsoft Word’s most powerful features. It enables you to personalise documents at scale. This is done by pulling data from a source like Excel. But did you know you can take personalisation a step further by embedding custom hyperlinks for each recipient? This technique is a game-changer for marketing campaigns, surveys, and event invitations. ### Prepare Your Data Source - Create an Excel sheet (or other supported source) with columns for names, emails, and a dedicated column for URLs. ### Set Up Mail Merge in Word - Go to **Mailings > Start Mail Merge** and select your document type (letters, emails, etc.). - Connect to your data source via **Select Recipients > Use an Existing List**. ### Insert Custom Hyperlinks - Place your cursor where you want the link. - Use **Insert > Hyperlink**. - In the “Address” field, insert the merge field for your URL column (e.g., `«Link»`). - The display text can be static (like “Click Here”) or dynamic (e.g., `«Name»’s Survey Link`). ### Preview & Test - Use **Preview Results** to ensure each recipient’s link is correctly embedded. - Test a few merged documents to confirm the hyperlinks are active. ## After sending emails After Word finishes to send all the emails, probably some of them are not deliverable. So, I want to have a list of the unavailable emails to remove them from the list. I don’t want to do it manually, so I wrote a script. ## What the script does 1. **It scans all the emails in your Inbox one by one.** 2. **Finds bounce‑back emails (NDRs)** - It checks the subject line for typical “failure” phrases like *Undeliverable* or *Delivery Status Notification*. - These are the messages Outlook/Exchange sends you when an email couldn’t be delivered. 3. **Filters by your original subject** - It only processes bounce‑backs that mention the subject of the email you care about (you set this in `subjectFilter = "Your Subject Here"`). - This way, you only collect failures related to that particular campaign or batch of emails. 4. **Extracts failed addresses** - It looks inside the body of the bounce message and uses a pattern (RegEx) to find anything that looks like an email address. - If it finds one, it saves it. - If it doesn’t, it falls back to using the `To` or `SenderEmailAddress` fields. 5. **Skips duplicates** - It uses a dictionary to make sure the same failed address isn’t added twice. 6. **Excludes certain domains** - You can keep a list of domains you don’t care about (e.g., `"exchangelabs.com"`, `"example.com"`) and the script will skip any addresses ending with those domains. 7. **Optional: Marks the bounce email as read** - There’s a variable (`markAsRead`) you can set to `True` or `False`. - If `True`, the script will mark each processed bounce message as “Read” so they don’t clutter your Inbox. 8. **Exports results to a CSV file** - After collecting all the failed addresses, it writes them into a file called `UnavailableEmails.csv` on your Desktop. - The file has one column called `FailedRecipient` with all the addresses listed. - You can open this file directly in Excel. 9. **Shows a completion message** - When it’s done, it pops up a message box telling you the export is complete and where the file is saved ## VBA script ``` Sub ExportUnavailableEmails() Dim ns As Outlook.NameSpace Dim inbox As Outlook.Folder Dim items As Outlook.items Dim itm As Object Dim subjectFilter As String Dim failedAddresses As Object Dim filePath As String Dim fso As Object, ts As Object ' === Configure === ' change the subject subjectFilter = "URGENT INPUT REQUIRED" filePath = Environ("USERPROFILE") & "\Desktop\UnavailableEmails.csv" ' Set to True if you want to mark emails as read, False to leave them Dim markAsRead As Boolean maskAsRead = True ' === Setup === Set ns = Application.GetNamespace("MAPI") Set inbox = ns.GetDefaultFolder(olFolderInbox) Set items = inbox.items items.Sort "[ReceivedTime]", False Set failedAddresses = CreateObject("Scripting.Dictionary") ' === Define excluded domains === Dim excludedDomains As Variant excludedDomains = Array("example.com", "testdomain.org") ' === Iterate items === Dim i As Long For i = items.Count To 1 Step -1 Set itm = items.item(i) If Not itm Is Nothing Then If itm.Class = olMail Or itm.Class = olReport Then If InStr(1, itm.Subject, "Undeliverable", vbTextCompare) > 0 _ Or InStr(1, itm.Subject, "Delivery Status Notification", vbTextCompare) > 0 Then If InStr(1, itm.Body, subjectFilter, vbTextCompare) > 0 Then Dim emails As Collection Set emails = ExtractEmailsFromText(itm.Body) Dim e As Variant For Each e In emails If Not IsExcludedDomain(e, excludedDomains) Then If Not failedAddresses.Exists(LCase$(e)) Then failedAddresses.Add LCase$(e), True End If End If Next e End If ' Mark as read if variable is True If maskAsRead Then itm.UnRead = False End If End If End If End If Next i ' === Write CSV === Set fso = CreateObject("Scripting.FileSystemObject") Set ts = fso.CreateTextFile(filePath, True) ts.WriteLine "FailedRecipient" Dim key As Variant For Each key In failedAddresses.Keys ts.WriteLine key Next key ts.Close MsgBox "Export complete! Saved to: " & filePath End Sub ' Helper: check if email ends with any excluded domain Private Function IsExcludedDomain(ByVal email As String, ByVal domains As Variant) As Boolean Dim d As Variant Dim addr As String addr = LCase$(Trim$(email)) For Each d In domains If Right$(addr, Len(d)) = LCase$(d) Then IsExcludedDomain = True Exit Function End If Next d IsExcludedDomain = False End Function ' Helper: extract emails with RegEx Private Function ExtractEmailsFromText(ByVal text As String) As Collection Dim re As Object, matches As Object, m As Object Set ExtractEmailsFromText = New Collection Set re = CreateObject("VBScript.RegExp") re.Pattern = "([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})" re.IgnoreCase = True re.Global = True If re.Test(text) Then Set matches = re.Execute(text) For Each m In matches ExtractEmailsFromText.Add m.Value Next m End If End Function ``` Happy coding! **Categories:** Microsoft Office, Tools **Tags:** microsoft-outlook **Hashtags:** microsoft-outlook --- ### [How to remove invalid emails from Outlook](https://puresourcecode.com/dotnet/visual-basic/how-to-remove-invalid-emails-from-outlook/) **Published:** October 7, 2025 **Author:** Enrico **Excerpt:** I’ll show you how to use a simple VBA macro. This macro will help you clean up your Outlook contact groups, by removing non-resolvable email **Content:** Managing contact groups in Outlook can be a time-saver. That is, until you hit send and half your recipients bounce back with “email not found” errors. If you’ve ever dealt with outdated or invalid emails in your distribution lists, this guide is for you. In this post, I’ll show you how to use a simple VBA macro. This macro will help you clean up your Outlook contact groups. It does this by removing non-resolvable email addresses. No more manual checking, no more guesswork. ## Why This Matters Outlook doesn’t automatically verify whether the email addresses in your contact groups are still valid. Over time, people change jobs, abandon old accounts, or mistype addresses. Sending to these outdated contacts can lead to: - Bounce-back errors - Missed communication - Wasted time ## Scenario In my Microsoft Outlook, I have a **Contact group** for recruiters. The turnover in the recruiting agency is quite high. When I send an email to the entire group, I receive a lot of errors in return. This happens because, for example, an agent is no longer working for that particular company. Consequently, the email is deactivated. There is no automatic function to remove those emails from the contact group. ## The VBA Macro That Does the Cleanup This macro scans each member of a selected contact group and removes those whose email addresses can’t be resolved. It then creates a new, cleaned-up version of the group. ``` Sub CleanContactGroup() Dim olApp As Outlook.Application Dim olNS As Outlook.NameSpace Dim olFolder As Outlook.Folder Dim olItem As Object Dim distList As Outlook.DistListItem Dim i As Long Dim tempList As Outlook.DistListItem Dim validMembers As Collection Dim member As Outlook.Recipient Set olApp = Outlook.Application Set olNS = olApp.GetNamespace("MAPI") Set olFolder = olNS.GetDefaultFolder(olFolderContacts) ' Prompt user to select a contact group Set olItem = olApp.ActiveExplorer.Selection.Item(1) If Not TypeOf olItem Is Outlook.DistListItem Then MsgBox "Please select a contact group in your Contacts folder.", vbExclamation Exit Sub End If Set distList = olItem Set validMembers = New Collection ' Check each member For i = 1 To distList.MemberCount Set member = distList.GetMember(i) If member.Resolve Then validMembers.Add member End If Next i ' Create a new contact group with valid members Set tempList = olFolder.Items.Add("IPM.DistList") tempList.DLName = distList.DLName & " - Cleaned" For Each member In validMembers tempList.AddMember member Next member tempList.Save MsgBox "Cleaned contact group created: " & tempList.DLName, vbInformation End Sub ``` ## How to Use It - Open Outlook and press Alt + F11 to launch the VBA editor. - Insert a new module and paste the code above. - Close the editor and return to Outlook. - Select the contact group you want to clean. - Run the macro from the Developer tab or assign it to a custom button. ## What This Macro Does (and Doesn’t Do) - ✅ Resolves each contact to check if it’s valid - ✅ Creates a new contact group with only valid members - ❌ Doesn’t verify if the email is deliverable (e.g., SMTP bounce detection) - ❌ Doesn’t remove members from the original group—your original stays intact If you need deeper validation, you’d need to integrate with your mail server. This might include checking if an email actually receives messages. Alternatively, you can use a third-party API. ## Wrap up This macro is a great way to keep your Outlook contact groups tidy and functional. It’s especially useful for teams, newsletters, or any recurring group communication. Clean lists mean fewer headaches—and fewer bounce-backs. If you’d like help customising this macro or integrating deeper validation, please don’t hesitate to reach out. **Categories:** Microsoft Office, Tools, Visual Basic **Tags:** contacts, microsoft-outlook --- ### [Building a MAUI CustomRefreshView](https://puresourcecode.com/dotnet/maui/building-a-maui-customrefreshview/) **Published:** September 10, 2025 **Author:** Enrico **Excerpt:** Solving the .NET MAUI RefreshView Limitation on macOS: building a MAUI CustomRefreshView from scratch for all platforms **Content:** Using MAUI for creating a real application presents a few real challenges. Creating a multiplatform application is not fully supported, unlike the RefreshView component. .NET MAUI is a powerful cross-platform UI framework, but not all controls are supported on every platform. One notable limitation is the absence of `RefreshView` support on macOS. This can be a blocker for developers who want a consistent pull-to-refresh experience across all platforms. In this post, I will: - Explain the issue with `RefreshView` on macOS. - Show how to build a cross-platform `CustomRefreshView` for .NET MAUI. - Provide step-by-step code and XAML integration. The full source code of this component is available on [GitHub](https://github.com/erossini/MauiRefreshView). ## The Problem: RefreshView on macOS The built-in `RefreshView` control in .NET MAUI enables pull-to-refresh functionality for scrollable content. However, as of .NET MAUI 9, `RefreshView` is **not supported on macOS**. Attempting to use it will result in runtime errors or simply no refresh gesture support. ### **Why does this happen?** - The underlying gesture and native control implementation for `RefreshView` is missing on macOS. - The official documentation and GitHub issues confirm this limitation. ### **Impact** - macOS users cannot trigger refresh actions via pull gestures. - UI consistency and user experience are affected. Here some Microsoft documentation about it: - [Specify the UI idiom for your Mac Catalyst app – .NET MAUI | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/maui/mac-catalyst/user-interface-idiom?view=net-maui-9.0) - [RefreshView Class (Microsoft.Maui.Controls) | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.refreshview?view=net-maui-9.0) - [Customize UI appearance based on the platform and device idiom – .NET MAUI | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/customize-ui-appearance?view=net-maui-9.0) ### Warning [UIStepper](/en-us/dotnet/api/uikit.uistepper), [UIPickerView](/en-us/dotnet/api/uikit.uipickerview), and [UIRefreshControl](/en-us/dotnet/api/uikit.uirefreshcontrol) aren’t supported in the Mac user interface idiom by Apple. This means that the .NET MAUI controls that consume these native controls are not usable in the Mac user interface idiom. These include the [Stepper](/en-us/dotnet/api/microsoft.maui.controls.stepper), [Picker](/en-us/dotnet/api/microsoft.maui.controls.picker), and [RefreshView](/en-us/dotnet/api/microsoft.maui.controls.refreshview). Attempting to do so will throw a macOS exception. In addition, the following constraints apply in the Mac user interface idiom: - [UISwitch](/en-us/dotnet/api/uikit.uiswitch) throws a macOS exception when it’s title is set in a non-Mac idiom view. - [UIButton](/en-us/dotnet/api/uikit.uibutton) throws a macOS exception when [AddGestureRecognizer](/en-us/dotnet/api/uikit.uiview.addgesturerecognizer) is called, or when [SetTitle](/en-us/dotnet/api/uikit.uibutton.settitle) or [SetImage](/en-us/dotnet/api/uikit.uibutton.setimage) are called for any state except `UIControlStateNormal.Normal`. - [UISlider](/en-us/dotnet/api/uikit.uislider) throws a macOS exception when the [SetThumbImage](/en-us/dotnet/api/uikit.uislider.setthumbimage), [SetMinTrackImage](/en-us/dotnet/api/uikit.uislider.setmintrackimage), [SetMaxTrackImage](/en-us/dotnet/api/uikit.uislider.setmaxtrackimage) methods are called and when the [ThumbTintColor](/en-us/dotnet/api/uikit.uislider.thumbtintcolor#uikit-uislider-thumbtintcolor), [MinimumTrackTintColor](/en-us/dotnet/api/uikit.uislider.minimumtracktintcolor#uikit-uislider-minimumtracktintcolor), [MaximumTrackTintColor](/en-us/dotnet/api/uikit.uislider.maximumtracktintcolor#uikit-uislider-maximumtracktintcolor), [MinValueImage](/en-us/dotnet/api/uikit.uislider.minvalueimage#uikit-uislider-minvalueimage), [MaxValueImage](/en-us/dotnet/api/uikit.uislider.maxvalueimage#uikit-uislider-maxvalueimage) properties are set. ## Solution: Build a CustomRefreshView To overcome this, you can create a custom control. It should mimic the core features of `RefreshView` and work on all platforms. This includes macOS. ### Features to Implement - Pull-to-refresh gesture detection - Visual feedback (spinner and optional text) - Command execution on refresh - Bindable properties for customization (color, position, background, etc.) ## Step 1: Create the CustomRefreshView Class Create a new file `CustomRefreshView.cs` in your MAUI project. ``` using System; using Microsoft.Maui.Controls; namespace YourApp.Components { public enum Position { Top, Middle, Bottom } public class CustomRefreshView : ContentView { // Bindable properties for IsRefreshing, RefreshCommand, RefreshColor, etc. // See full code below for all properties // Internal controls private readonly ActivityIndicator _activityIndicator; private readonly Label _indicatorLabel; private readonly Grid _grid; private readonly VerticalStackLayout _indicatorStack; private double _totalY; public CustomRefreshView() { // Gesture detection var panGesture = new PanGestureRecognizer(); panGesture.PanUpdated += OnPanUpdated; GestureRecognizers.Add(panGesture); // Spinner _activityIndicator = new ActivityIndicator { IsVisible = false, IsRunning = false, VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center, InputTransparent = true }; _activityIndicator.SetBinding(ActivityIndicator.ColorProperty, new Binding(nameof(RefreshColor), source: this)); // Optional text _indicatorLabel = new Label { IsVisible = false, VerticalOptions = LayoutOptions.Center, HorizontalOptions = LayoutOptions.Center }; // Stack for spinner and text _indicatorStack = new VerticalStackLayout { HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, IsVisible = false, Children = { _activityIndicator, _indicatorLabel } }; // Grid for positioning _grid = new Grid { RowDefinitions = { new RowDefinition { Height = GridLength.Star }, new RowDefinition { Height = GridLength.Star }, new RowDefinition { Height = GridLength.Star } } }; _grid.Children.Add(_indicatorStack); Grid.SetRow(_indicatorStack, 1); // Default: Middle Content = _grid; } // ... Bindable properties and property changed handlers ... // See full code below } ``` ## Step 2: Add Bindable Properties Add properties for customization and MVVM support: - `IsRefreshing` (bool) - `RefreshCommand` (Command) - `RefreshColor` (Color) - `IndicatorText` (string) - `IndicatorTextColor` (Color) - `IndicatorBackground` (Color) - `IndicatorPosition` (enum: Top, Middle, Bottom) - `IndicatorMargin`, `IndicatorMinimumWidthRequest`, `IndicatorMinimumHeightRequest` (layout) This is an example for the `IsRefreshing` property ``` public static readonly BindableProperty IsRefreshingProperty = BindableProperty.Create( nameof(IsRefreshing), typeof(bool), typeof(CustomRefreshView), false, propertyChanged: OnIsRefreshingChanged); public bool IsRefreshing { get => (bool)GetValue(IsRefreshingProperty); set => SetValue(IsRefreshingProperty, value); } ``` ## Step 3: Handle the Pull-to-Refresh Gesture Detect a downward pan gesture and trigger the refresh command: ``` private void OnPanUpdated(object sender, PanUpdatedEventArgs e) { switch (e.StatusType) { case GestureStatus.Started: _totalY = 0; break; case GestureStatus.Running: _totalY += e.TotalY; break; case GestureStatus.Completed: if (_totalY > 50) RefreshCommand?.Execute(null); break; } } ``` ## Step 4: Show Spinner and Text When Refreshing Update visibility and appearance based on `IsRefreshing` and other properties: ``` private static void OnIsRefreshingChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is CustomRefreshView control) { bool isRefreshing = (bool)newValue; control._activityIndicator.IsRunning = isRefreshing; control._activityIndicator.IsVisible = isRefreshing; control._indicatorLabel.IsVisible = isRefreshing && !string.IsNullOrEmpty(control.IndicatorText); control._indicatorStack.IsVisible = isRefreshing; } } ``` ## Step 5: Position the Indicator Group Use the grid to position the indicator at the top, middle, or bottom: ``` private static void OnIndicatorPositionChanged(BindableObject bindable, object oldValue, object newValue) { var control = (CustomRefreshView)bindable; var position = (Position)newValue; switch (position) { case Position.Top: control._indicatorStack.VerticalOptions = LayoutOptions.Start; break; case Position.Bottom: control._indicatorStack.VerticalOptions = LayoutOptions.End; break; default: control._indicatorStack.VerticalOptions = LayoutOptions.Center; break; } } ``` ## Step 6: Use the CustomRefreshView in XAML ``` ``` ## Conclusion By building a `CustomRefreshView`, you can provide a consistent pull-to-refresh experience across all .NET MAUI platforms, including macOS. This approach is flexible, customizable, and future-proof for your cross-platform applications. **Key takeaways:** - `RefreshView` is not supported on macOS in .NET MAUI. - A custom control can replicate its functionality using gesture detection and MVVM-friendly properties. - The solution is fully cross-platform and highly customizable. Happy coding! **Categories:** .NET9, MAUI **Tags:** maui, maui-component, net9, refreshview --- ### [Building a Custom Matching Pairs Component in .NET MAUI](https://puresourcecode.com/dotnet/maui/building-a-custom-matching-pairs-component-in-net-maui/) **Published:** September 2, 2025 **Author:** Enrico **Excerpt:** We will walk through the design and implementation of a flexible, reusable Matching Pairs component for .NET9 MAUI **Content:** In this post, we’ll walk through the design and implementation of a flexible, reusable Matching Pairs component for .NET MAUI. This journey was driven by real-world requirements. Iterative improvements contributed as well. The result is a highly customizable and MVVM-friendly UI control. It is suitable for educational games, language learning, and more. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/09/matching.gif?resize=640%2C402&ssl=1) The full source code is available on [GitHub](https://github.com/erossini/MauiMatchingPairs). ## The Goal We wanted a component that: - Displays two columns of selectable items (e.g., words or phrases). - Lets the user select one item from each column to attempt a match. - Provides immediate visual feedback (color changes) for correct and incorrect matches. - Disables matched pairs and tracks the number of attempts. - Exposes a customizable “Continue” button, only enabled when all pairs are matched. - Is highly customizable via bindable properties for colors, styles, text, and behavior. - Supports both MVVM commands and event handlers for button actions. --- ## Step 1: The Data Model We started with a simple `MatchingPair` class, then extended it to support UI state and property change notifications: ``` public class MatchingPair : INotifyPropertyChanged { public string Left { get; set; } public string Right { get; set; } // ... properties for IsMatched, IsLeftEnabled, IsRightEnabled, LeftColor, RightColor, etc. // Implements INotifyPropertyChanged for UI updates } } ``` Each item is rendered with a `Border` and a `Label`, with colors and enabled state bound to the model. ## Step 2: The UI Layout The component’s XAML uses a `Grid` with two `CollectionView`s for the left and right columns, and a `Button` for confirmation: ``` ``` ## Step 3: Customization via Bindable Properties To make the component flexible, we exposed a wide range of bindable properties, including: - **Colors**: For correct, wrong, selected, and default states (both border and text). - **Button**: Text, style, command, and event handler. - **Delay**: Feedback display duration (in ms). - **ShowAttempts**: Whether to display the attempt count. - **AttemptCountStringFormat**: Custom format for the attempt label. - **Pairs**: The collection of pairs to match. This allows consumers to fully tailor the component’s appearance and behavior. --- ## Step 4: Selection and Matching Logic The core logic ensures: - Only one item per column can be selected at a time. - When two items are selected, `TryMatch` is called. - The component disables both `CollectionView`s during feedback (using a private `_isBusy` flag and a helper method). - Visual feedback is shown for a configurable delay. - Matched pairs are disabled and colored appropriately. - The attempt count is incremented and can be displayed. - The “Continue” button is only enabled when all pairs are matched. Example of the optimized `TryMatch` method: ``` private async void TryMatch() { if (selectedLeft == null || selectedRight == null || _isBusy) return; _isBusy = true; SetCollectionsEnabled(false); AttemptCount++; var leftMatch = LeftWords.FirstOrDefault(x => x.Left == selectedLeft.Left); var rightMatch = RightWords.FirstOrDefault(x => x.Right == selectedRight.Right); if (leftMatch == null || rightMatch == null) { _isBusy = false; SetCollectionsEnabled(true); return; } if (selectedLeft.Right == selectedRight.Right) { // Correct match feedback leftMatch.LeftColor = CorrectColor; leftMatch.LeftTextColor = CorrectTextColor; leftMatch.IsMatched = true; leftMatch.IsLeftEnabled = false; rightMatch.RightColor = CorrectColor; rightMatch.RightTextColor = CorrectTextColor; rightMatch.IsMatched = true; rightMatch.IsRightEnabled = false; } else { // Wrong match feedback leftMatch.LeftColor = WrongColor; leftMatch.LeftTextColor = WrongTextColor; rightMatch.RightColor = WrongColor; rightMatch.RightTextColor = WrongTextColor; } await Task.Delay(Delay); if (selectedLeft.Right != selectedRight.Right) { // Reset colors after wrong match leftMatch.LeftColor = DefaultStrokeColor; leftMatch.LeftTextColor = DefaultTextColor; rightMatch.RightColor = DefaultStrokeColor; rightMatch.RightTextColor = DefaultTextColor; } selectedLeft = null; selectedRight = null; LeftCollection.SelectedItem = null; RightCollection.SelectedItem = null; UpdateAllMatched(); if (!AllMatched) SetCollectionsEnabled(true); _isBusy = false; } ``` ## Step 5: Button Actions The “Continue” button supports both MVVM and code-behind patterns: - **ButtonConfirmCommand**: Bind to an `ICommand` in your ViewModel. - **ButtonConfirmClicked**: Attach an event handler in code-behind. Both are triggered when the button is clicked. ## Step 6: Accessibility and Usability - The component disables selection while feedback is being shown. - Once all pairs are matched, the button is enabled and the collections are disabled. - All UI text and colors are customizable for accessibility and localization. ## Step 7: Usage in a Page Here’s how you can use the component in your MAUI page: ``` ``` ## Step 8: Full List of Parameters Property NameTypeDefaultDescription`Pairs`ObservableCollection—The list of pairs to match.`ShowAttempts`bool`true`Whether to display the attempt count label.`AttemptCount`int`0`The number of attempts made (read-only, updated by the component).`AllMatched`bool`false`Indicates if all pairs have been matched (read-only, updated by the component).`Delay`int`200`Delay in milliseconds for feedback display after a match attempt.`ButtonConfirmText`string`"Continue"`The text displayed on the confirm button.`ButtonConfirmStyle`Style`null`The style applied to the confirm button.`ButtonConfirmCommand`ICommand`null`Command executed when the confirm button is clicked.`ButtonConfirmClicked`EventHandler`null`Event raised when the confirm button is clicked.`CorrectColor`Color`Green`Border color for a correct match.`CorrectTextColor`Color`Green`Text color for a correct match.`WrongColor`Color`Red`Border color for an incorrect match.`WrongTextColor`Color`Red`Text color for an incorrect match.`SelectedBorderColor`Color`DodgerBlue`Border color for a selected item.`SelectedTextColor`Color`DodgerBlue`Text color for a selected item.`DefaultStrokeColor`Color`#FF444444`Default border color for items.`DefaultBackgroundColor`Color`White`Default background color for items.`DefaultTextColor`Color`White`Default text color for items.--- ## Conclusion Through iterative improvements and a focus on flexibility, we’ve built a robust Matching Pairs component for .NET MAUI. It’s easy to use, highly customizable, and ready for integration into your next educational or gamified app. **Happy coding!** **Categories:** .NET9, MAUI **Tags:** matching, maui, maui-component, net9 --- ### [The Commodore 64 is back](https://puresourcecode.com/news/the-commodore-64-is-back/) **Published:** July 21, 2025 **Author:** Enrico **Excerpt:** One of the most iconic and important computers of the 1980s is set to make a comeback. All-new Commodore 64 is planned for release this year **Content:** One of the most iconic and important computers of the 1980s is set to make a comeback. An all-new [Commodore 64](https://www.commodore.net/) is planned for release later this year. The announcement comes after YouTuber Christian Simpson (AKA Peri Fractic) successfully purchased the Commodore name. He also acquired trademarks, patents, and licenses in June. Simpson paid in the “low seven figures” for Commodore. He hasn’t wasted any time revealing the company’s initial plans. The first thing on the agenda is seemingly the relaunch of the much-loved C64. Called the Commodore 64 Ultimate, the new version will come in three variants. These variants are Basic Beige, Starlight Edition, and the Founders Edition. They each essentially do the exact same thing but have different lighting/colour effects. The Basic Beige model looks exactly like the C64 of the 80s, save for some new, modern ports and capabilities. The Starlight Edition is transparent. It contains LED lighting that can change colours. The Founders Edition is also transparent. It glows in gold and features 24k gold badges and a holographic serial number sticker. Only 6,400 units of the latter will be made, so it’s a true collector’s edition. The computer itself is essentially identical with each version. It will be able to play the 10,000+ C64 games available today. It has more RAM than the original. You can also use the optional 48 MHz Turbo mode. Those games can be stored on just about anything. They can be on the old discs, cartridges, tapes, or even just run from USB storage. The C64 Ultimate comes with a USB that looks like an old cassette. It is filled with more than 50 games. You can just use a file browser to load and play them. A brand-new game is being made for the machine’s launch. It is a sequel to Jupiter Lander: Ascension (Commodore’s first ever game). In terms of specs, the Commodore 64 Ultimate will support 1080p out over HDMI, plus three USB-A 2.0 ports and one USB-C. There’s a microSD card slot too for expansion. Wi-Fi is built in along with Ethernet for wired internet connectivity. All three models are available to [preorder on the official Commodore webpage](https://www.commodore.net/product-page/commodore-64-ultimate-basic-beige-batch1) right now. The Basic Beige version is priced at an early bird bonus of $299.99 / £223.89 / €257.99 / AU$458.98. The Starlight Edition is $349.99/ £261.09 / €300.99 / $535.48, while the Founders Edition is the priciest, at $499.99 / £372.99 / €429.99 / AU$764.98. The first batch of machines is rapidly selling out. It’s proving to be very popular. They are expected to ship from October/November. It’s not the first reissue of the Commodore 64. That was [TheC64 from Retro Games in 2022](https://www.amazon.co.uk/Koch-Distribution-RGLA03-UK-61D2-The-C64/dp/B09Z7R81MV). However, it’s from the all-new Commodore company. Hopefully that makes it as authentic as can be. **Categories:** News, Other **Tags:** c64, commodore **Hashtags:** c64, commodore --- ### [MAUI component for skills using FlexLayout](https://puresourcecode.com/dotnet/maui/maui-component-for-skills-using-flexlayout/) **Published:** July 9, 2025 **Author:** Enrico **Excerpt:** I will create a new MAUI reusable component for list of strings using FlexLayout with add and remove functionalities. **Content:** In this new post, I will create a new MAUI component for skills using `FlexLa`yout. For that, I am using the code from my previous post [How to use FlexLayout with different sizes](https://puresourcecode.com/dotnet/maui/how-to-use-flexlayout-with-different-sizes/). The full source code is available on [GitHub](https://github.com/erossini/MAUIFlexSkillsComponent). ## What I want to do In my previous post [How to use FlexLayout with different sizes](https://puresourcecode.com/dotnet/maui/how-to-use-flexlayout-with-different-sizes/), I created a MAUI application. It displays text in pills. It also includes a button to remove the values. For that, I use a `FlexLayout` to display the pills. So, it is possible to have a long list of words on multiple lines. Now, I want to create a reusable component from this code. Here it is the list of expectations: - gets a reusable component for displaying text in pills - adds or removes items - returns the list of `string` ## Implementation ### Create EntryChoices.xaml First, I’m going to create a new folder called **Components** where to add all the components for a project. That I create a new ComponentView called `EntryChoices.xaml`. In the XAML, I am going to add the `VerticalStackLayout` what was in the `MainPage.xaml`. ``` ``` ### Add the Resources After that, I have to add the converters to use in the XAML. So, I add this code ``` ``` and then in the `ContentView`, I need to add the reference to the `converter`. ``` ``` Now, the name of the `ContentView` is required. This will help us in the binding reference in the rest of the code. ### Add the Reference Now, it is necessary to bind the XAML with code behind. For that, I have to add the `Reference` to the `VerticalStackLayout` like in the following code ``` ``` ## Create the property Now, the next step is to create the `BindableProperty` for the list of strings. So, in the code behind in the file `EntryChoices.xaml.cs`, I add the following code: ``` public static readonly BindableProperty ItemsProperty = BindableProperty.Create( propertyName: nameof(Items), // Property name returnType: typeof(ObservableCollection), // Property type declaringType: typeof(EntryChoices), // Declaring type defaultValue: new ObservableCollection(), // Default value propertyChanged: OnItemsChanged, // Optional: PropertyChanged callback defaultBindingMode: BindingMode.TwoWay ); ``` This defines the `ItemsProperty` that is an `ObservableCollection`. Also, I defined the properties `Items` and `OnItemsChanged` to handle the update of the values. The default binding mode is two ways. ### Create the CLR property wrapper Now, I have to add the wrapper for the `Items` to handle the list of strings. ``` public ObservableCollection Items { get => (ObservableCollection)GetValue(ItemsProperty); set => SetValue(ItemsProperty, value); } ``` The `ObservableCollection` gives us the support for updating the UI. ### Handle property changes So, the next step is to handle the property changes. ``` private static void OnItemsChanged(BindableObject bindable, object oldValue, object newValue) { if (bindable is EntryChoices customView) { // Handle the property change logic here customView.OnItemsUpdated((ObservableCollection)newValue); } } private void OnItemsUpdated(ObservableCollection newValue) { // Example: Update UI or perform actions based on the new value Console.WriteLine($"Items updated to: {newValue}"); } ``` ## Add and remove items Now, the last thing to do is to handle the buttons to add or remove a text from the list. Here the code for it: ``` private void OnDeleteSkillClicked(object sender, TappedEventArgs e) { string skill = (sender as Image).BindingContext as string; if (!string.IsNullOrEmpty(skill)) { Items.Remove(skill); } } private void OnAddSkillClicked(object sender, TappedEventArgs e) { if (!string.IsNullOrWhiteSpace(ItemEntry.Text)) { Items.Add(ItemEntry.Text); ItemEntry.Text = ""; } } ``` ## How to use this component Now, in the `MainPage.xaml`, I add the reference to the component like that ``` ``` And then I can add the following code ``` ``` ### ViewModel Now, the page uses a ViewModel defined like the following ``` public class MainPageViewModel { private ObservableCollection? _skills; public MainPageViewModel() { Skills = new ObservableCollection() { "test1", "test2" }; } public ObservableCollection? Skills { get { return _skills; } set { _skills = value; } } } ``` ## Wrap up In conclusion, in this post, I created a MAUI component for displaying list of strings (skills) in pills using FlexLayout. Let me know your feedback and comment. Happy coding! **Categories:** MAUI **Tags:** components, maui, maui-component **Hashtags:** maui, maui-component --- ### [Cloning all repositories from Azure DevOps](https://puresourcecode.com/tools/powershell/cloning-all-repositories-from-azure-devops/) **Published:** June 30, 2025 **Author:** Enrico **Excerpt:** I will provide a script for cloning all repositories from Azure DevOps in an organisation with projects and repositories using PowerShell **Content:** I will provide a script for cloning all repositories from [Azure DevOps](https://puresourcecode.com/category/tools/azure-devops/) in an organisation with multiple projects and repositories using PowerShell. ## Scenario Often, I end up with a big mess in the repositories from a project at work. I want to download all the repositories again from a particular organisation. For example, I used to split my code into NuGet packages to manage the projects easily. Sometimes, I need to replace the NuGet packages with the projects to verify a bug. There is another case. As a contractor, when I arrive at a customer site, the companies provide me access to several projects. These projects are in Azure DevOps. These projects include repositories that I’m supposed to contribute to. In other cases, I just want to have a copy of all the repositories to save the code. In all of these scenarios, I find myself copy-pasting URLs and running git clone a few times. I know, it doesn’t sound too hard, but I dislike doing things more than once! ## My solution There’s an extension to Azure CLI for integrating with Azure DevOps, which I’ve found really handy. Not many people are aware of the vast number of extensions available to the Azure CLI. Now, I want to show you how I can use this. It allows me to clone all Azure repositories I have access to in Azure DevOps. I assume we already have Azure CLI installed; if not, it’s time to [Install the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest). First, we need to sign in. This is done by calling ``` az login ``` If I don’t have access to any subscriptions in [Azure](https://puresourcecode.com/tools/azure-devops/azure-devops-processes/), I’ll have to add the parameter `–allow-no-subscriptions`. ``` az login --allow-no-subscriptions ``` Then, we need to make sure we have the right extension installed. Installed extensions can be listed with the command: ``` az extension list ``` If there are many extensions installed, this list can be overwhelming. I convert the output from JSON to objects using PowerShell. Then, I would filter the list as I would anything else in PowerShell. So, there’s a native way of filtering in the Azure CLI, so why not use that? The Azure CLI has a parameter called `–query` that takes a **JMESPath** expression. JMESPath is similar to XPath, but for JSON instead of XML; it is documented over at . The response I get from listing extensions is an array of extensions. To filter that, I can use the following expression: `[?name == ‘‘azure-devops’’].name`. I can also instruct the Azure CLI to output the result in a tab-separated format. I do this by adding the parameter `-o tsv` instead of JSON. This suits me well here since I’ll just get one name back. ``` az extension list --query '[?name == ''azure-devops''].name' -o tsv ``` If from the list I can get **azure-devops** back, I’m good to go! If I get nothing back, I need to install the DevOps extension. This is done by running: ``` az extension add --name 'azure-devops' ``` Now, we’re ready to shoot some queries to Azure DevOps! To list repositories in Azure DevOps, we need to supply the organisation URL and the project name. Let’s store the organisation name in a variable and use that to list all projects. Once again, we use a query to get only the names back and choose to get output as TSV. ``` $Organization = 'https://dev.azure.com/enricorossini' $Projects = az devops project list --organization $Organization --query 'value[].name' -o tsv ``` Now, I’m ready to list all repositories in each project! ``` foreach ($Proj in $Projects) { az repos list --organization $Organization --project $Proj | ConvertFrom-Json } ``` ## The full script This will output an object for each repository. The path to clone using HTTPS if found in the property `webUrl`. Now, if we combine this, we get the following script: ``` param( [string]$Organization ) if ($Organization -notmatch '^https?://dev.azure.com/\w+') { $Organization = "https://dev.azure.com/$Organization" } # Make sure we are signed in to Azure $AccountInfo = az account show 2>&1 try { $AccountInfo = $AccountInfo | ConvertFrom-Json -ErrorAction Stop } catch { az login --allow-no-subscriptions } # Make sure we have Azure DevOps extension installed $DevOpsExtension = az extension list --query '[?name == ''azure-devops''].name' -o tsv if ($null -eq $DevOpsExtension) { $null = az extension add --name 'azure-devops' } $Projects = az devops project list --organization $Organization --query 'value[].name' -o tsv foreach ($Proj in $Projects) { if (-not (Test-Path -Path ".\$Proj" -PathType Container)) { New-Item -Path $Proj -ItemType Directory | Select-Object -ExpandProperty FullName | Push-Location } $Repos = az repos list --organization $Organization --project $Proj | ConvertFrom-Json foreach ($Repo in $Repos) { if(-not (Test-Path -Path $Repo.name -PathType Container)) { Write-Warning -Message "Cloning repo $Proj\$($Repo.Name) [$($Repo.webUrl)]" git clone $Repo.webUrl } } } ``` ## Script for a list of repositories So, with the above script, cloning all repositories from Azure DevOps is quite easy. Now, consider I want to add the list of repositories in a Wiki page: I want to have the name of each repository with the link. For that, I have modified the script and ``` param( [string]$Organization ) if ($Organization -notmatch '^https?://dev.azure.com/\w+') { $Organization = "https://dev.azure.com/$Organization" } # Make sure we are signed in to Azure $AccountInfo = az account show 2>&1 try { $AccountInfo = $AccountInfo | ConvertFrom-Json -ErrorAction Stop } catch { az login --allow-no-subscriptions } # Make sure we have Azure DevOps extension installed $DevOpsExtension = az extension list --query '[?name == ''azure-devops''].name' -o tsv if ($null -eq $DevOpsExtension) { $null = az extension add --name 'azure-devops' } $list = "" $cmd = "" $Projects = az devops project list --organization $Organization --query 'value[].name' -o tsv foreach ($Proj in $Projects) { if (-not (Test-Path -Path ".\$Proj" -PathType Container)) { New-Item -Path $Proj -ItemType Directory | Select-Object -ExpandProperty FullName | Push-Location } $Repos = az repos list --organization $Organization --project $Proj | ConvertFrom-Json foreach ($Repo in $Repos) { if(-not (Test-Path -Path $Repo.name -PathType Container)) { $list = $list + "[" + $Repo.Name + "](" + $Repo.webUrl + ")`n" $cmd = $cmd + "git clone " + $Repo.webUrl + "`n" } } } Write-Host "List of repositories" Write-Host $list Write-Host "`n`n-------`n`n" Write-Host "Command to execute:" Write-Host $cmd ``` ## Wrap up In conclusion, I show a script in this post. It clones all repositories from Azure DevOps. It also lists them in Markdown format. I hope those scripts can help you. Keep in touch! Happy coding! **Categories:** Azure DevOps, PowerShell **Tags:** azure-cli, azure-devops, powershell, project, repositories **Hashtags:** powershell --- ### [WebApiDocumentator: an alternative to Swagger](https://puresourcecode.com/dotnet/webapi/webapidocumentator-an-alternative-to-swagger/) **Published:** May 27, 2025 **Author:** Enrico **Excerpt:** The NuGet package WebApiDocumentator is an alternative to Swagger and the design is simple and more useful for developers and end users **Content:** Searching for a better visualisation for my APIs, I was looking for an alternative to [Swagger](https://puresourcecode.com/?s=swagger). `WebApiDocumentator` is a quick and easy way to create an interface to document a WebAPI built in .NET Core. It creates a user-friendly interface and has options for endpoint testing. I used Swagger for a long time. Working with programmers who aren’t from the .NET Core world. Also, I realized that Swagger isn’t always very clear when it comes to documenting APIs. When using a well-documented public or private API, you realize the shortcomings of Swagger. It really leaves a lot to be desired. For an external programmer, knowing exactly how to make requests can be confusing with Swagger. This is especially true because it orders endpoints based on files. It does not order them by generated paths. ## WebApiDocumentator So, I found this package created by [Sergi Ortiz Gomez](https://github.com/drualcman) that it is quite cool. The source code of this package is available on [GitHub](https://github.com/drualcman/WebApiDocumentator). The features of this NuGet package are: - Automatic documentation using XML metadata from C# code - Show endpoints tree structure - HTML interface - Test endpoints ## Installation[](https://github.com/drualcman/WebApiDocumentator#installation) Install the NuGet package via the package manager: ``` dotnet add package WebApiDocumentator ``` Or by using the NuGet CLI ``` nuget install WebApiDocumentator ``` ## Quick Start[](https://github.com/drualcman/WebApiDocumentator#quick-start) ### Step 1: Add WebApiDocumentator to Your API[](https://github.com/drualcman/WebApiDocumentator#step-1-add-webapidocumentator-to-your-api) In your `Program.cs`, you will need to add the middleware to your service collection and configure the options. #### 1.1 Configure Options[](https://github.com/drualcman/WebApiDocumentator#11-configure-options) You can customize the url for the page and add basic data via `DocumentatorOptions`. In appsettings json using `IOptions` file like: ``` "DocumentatorOptions": { "ApiName": "Your api name", "Version": "Your version, it's a string", "Description": "Full descripcion about your API", "DocsBaseUrl": "documentation path, defatul it's [api root]/WebApiDocumentator" } ``` Or directly like: ``` public void ConfigureServices(IServiceCollection services) { services.AddWebApiDocumentator(options => { options.ApiName = "Test Api"; options.Version = "v1"; options.Description = "The best API in the world!"; options.DocsBaseUrl = "docs/api" }); } //minimal api builder.Services.AddWebApiDocumentator(); //Or also can do to read the appsettings.json /* builder.Services.AddWebApiDocumentator( options => builder.Configuration.GetSection(SmartCacheOptions.SectionKey).Bind(options) ); */ ``` #### 1.2 Add the user interface[](https://github.com/drualcman/WebApiDocumentator#12-add-the-user-interface) In your `Configure` method, add the middleware to the pipeline using `UseWebApiDocumentator()`: ``` public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Add other middlewares like routing, authentication, etc. app.UseWebApiDocumentator(); } //minimal api app.UseWebApiDocumentator(); ``` ### Step 2: Interface HTML[](https://github.com/drualcman/WebApiDocumentator#step-2-interface-html) To access to the interface you can use the default URL ``` [your api url]/WebApiDocumentator ``` Or if you personalise the URL then use your own URL ``` [your api url]/docs/api ``` *Remind:* You can always use a default WebApiDocumentator page. #### 2.1 Home page[](https://github.com/drualcman/WebApiDocumentator#21-home-page) - Show the name, version and description from your options. - Show the schema of your API - Right side bar to search and select endpoints #### 2.2 Selected endpoint[](https://github.com/drualcman/WebApiDocumentator#22-selected-endpoint) - Show documentation information - Show parameters type and from where - Show testing tab ## Example of the output ![Home page for the APIs in the application - An alternative to Swagger](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-50.png?resize=640%2C378&ssl=1)Home page for the APIs in the application ![The details page for a specific API - Search in the API list](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-51.png?resize=640%2C257&ssl=1)The details page for a specific API – Search in the API list ![The details of an API with the code example](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-53.png?resize=640%2C258&ssl=1)The details of an API with the code example ![I can test the endpoint from the UI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-55.png?resize=640%2C176&ssl=1)I can test the endpoint from the UI **Categories:** .NET8, .NET9, WebAPI **Tags:** swagger, swagger-ui, WebApiDocumentator --- ### [Resilient connection to RabbitMQ](https://puresourcecode.com/dotnet/net7/resilient-connection-to-rabbitmq/) **Published:** May 26, 2025 **Author:** Enrico **Excerpt:** The connection with RabbitMQ isn't always stable. The reconnection not always is working. I want to create a resilient connection to RabbitMQ **Content:** The connection with RabbitMQ is not always stable and the automatic re-connection not always is working and for this reason i want to create a resilient connection to RabbitMQ. The source code of this post is available on [GitHub](https://github.com/erossini/RabbitMQConnection). ## What is RabbitMQ? **RabbitMQ** is an open-source message broker that implements the **AMQP (Advanced Message Queuing Protocol)** protocol. It is a messenger between applications that is responsible for communication by sending and receiving messages. RabbitMQ is developed in the **Erlang programming language**. This is why it is efficient and capable of handling a large number of concurrent connections. ## Core components of RabbitMQ To understand **RabbitMQ** in depth, we need to understand the core components of RabbitMQ first. Next, we will explore RabbitMQ core components. ### 1. Producer A producer in RabbitMQ is an application that is responsible for sending messages to the **RabbitMQ server**. These messages can carry various kinds of data, such as JSON, XML, or plain text. ### 2. Queue In RabbitMQ, a Queue can be referred to as temporary or buffered storage of messages. RabbitMQ manages a queue, where messages are stored before they are consumed by the receiver. Queues can be durable, which means they persist across restarts. They can also be transient. This means they are deleted when the server restarts. ### 3. Consumer As the producer is responsible for sending, the Consumer in RabbitMQ reads messages from the queue. It also processes the messages. On receiving the message consumer can acknowledge, which means it tells RabbitMQ that the message has been processed successfully. ### 4. Exchange An **exchange** is responsible for routing messages among queues. **RabbitMQ provides support for various kinds of exchanges such as direct, fanout, topic, and headers, each queue with its logic.** It supports different types of exchanges listed below - **Direct Exchange:** In this exchange, messages are being routed based on routing keys. This means that when the routing key matches the binding key, the message gets delivered to its corresponding queue. - **Fanout Exchange:** With this exchange, RabbitMQ does the broadcasts to all messages that are bound to it regardless of the routing key. - **Topic Exchange:** With topic exchange message routing happens based on pattern matching to the routing and binding key. - **Headers Exchange:** In this route messages are based on headers instead of the routing key. The headers exchange is more flexible but less efficient compared to other exchange types. ### 5. Binding In **RabbitMQ** a connection between queue and exchange is called **binding**. The message route from the exchange to the queue is defined by binding. This is based on specific criteria such as routing keys. ### 6. Routing Key The routing key plays a very important role in **RabbitMW**. The exchange in RabbitMQ uses the routing key attribute. It determines how to route the message to the appropriate queue(s). It ensures the correct message between exchanges. ## Install RabbitMQ The best place to start is [RabbitMQ’s website](https://www.rabbitmq.com/). Here, you can find all the documentation you need and links to the download from [GitHub](https://github.com/rabbitmq/rabbitmq-server). The version I downloaded to write this post is [4.1.0](https://github.com/rabbitmq/rabbitmq-server/releases/tag/v4.1.0). Download the executable and run it. Among the other files, the setup registers a Windows Service call RabbitMQ that provides the entire system. This is enough to play with the services offered by RabbitMQ. As a human, I want to see the queue. I also want to manage them. ## Install the RabbitMQ Management UI Now, to install the RabbitMQ Management UI, if you use Windows, select the RabbitMQ Command Prompt option. ![RabbitMQ Command Prompt - Resilient connection to RabbitMQ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-29.png?resize=640%2C719&ssl=1)RabbitMQ Command Prompt Then, you have to run the following command to enable the UI: ``` rabbitmq-plugins enable rabbitmq_management ``` If you open a browser, it is possible to see the manager tool using this URL: ``` http://localhost:15672/ ``` Now, at this address, you see the following web page: ![RabbiqMQ Login Page - Resilient connection to RabbitMQ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-31.png?resize=640%2C414&ssl=1) So, to access for the first time, the credentials to use are: ``` Username: guest Password: guest ``` Now, you see the overview page with the list of all services and queues. ![RabbitMQ Overview - Resilient connection to RabbitMQ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-30.png?resize=640%2C414&ssl=1)RabbitMQ Overview ## Where is RabbitMQ? The installation of RabbitMQ will register a Windows Service on the machine. This service manages all the activities and also organizes the extensions like the Management UI. ![In the Windows Service list there is RabbitMQ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-49.png?resize=640%2C346&ssl=1)In the Windows Service list there is RabbitMQ This is good to know for testing. So, I can stop this service when I want to test the broken connection. ## The first connection Now, RabbitMQ is up and running and I can write my first basic application. So, open Visual Studio and create a new **Console Application**. After that, add the official client from NuGet.org, and the version I use for this post is 7.1.2 ``` dotnet add package RabbitMQ.Client --version 7.1.2 ``` First thing to do is to create a `ConnectionFactory` to connect to the RabbitMQ server. ``` var factory = new ConnectionFactory() { HostName = "localhost" }; ``` Generally speaking, we will prefer to read the `HostName` from the configuration. Then, I have to create a connection and a communication channel between my application and RabbitMQ. This is the code ``` using (var connection = await factory.CreateConnectionAsync()) using (var channel = await connection.CreateChannelAsync()) { } ``` Now, I want to create my first queue with the name `testqueue`. So, I use `QueueDeclareAsync` function for that ``` await channel.QueueDeclareAsync("testqueue", true, false, false, null); ``` Next, I have to create a consumer for the queue based on events on the `testqueue` I have just created. If the queue does not exist, RabbitMQ will create the queue. When a message is posted in the queue and the client receives it, I want to display the message. For that the code is the following: ``` var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += Consumer_ReceivedAsync; await channel.BasicConsumeAsync("testqueue", true, consumer); ``` Now, I am going to create the `Consumer_ReceivedAsync` function to receives the message content and display on the screen ``` async Task Consumer_ReceivedAsync(object sender, BasicDeliverEventArgs ev) { var body = ev.Body; var content = Encoding.UTF8.GetString(body.ToArray()); Console.WriteLine(content); return content; } ``` ### Full code The full source code of this first implementation. ``` using RabbitMQ.Client; using RabbitMQ.Client.Events; using System.Text; var factory = new ConnectionFactory() { HostName = "localhost" }; using (var connection = await factory.CreateConnectionAsync()) using (var channel = await connection.CreateChannelAsync()) { await channel.QueueDeclareAsync("testqueue", true, false, false, null); var consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += Consumer_ReceivedAsync; await channel.BasicConsumeAsync("testqueue", true, consumer); Console.ReadLine(); } async Task Consumer_ReceivedAsync(object sender, BasicDeliverEventArgs ev) { var body = ev.Body; var content = Encoding.UTF8.GetString(body.ToArray()); Console.WriteLine(content); return content; } ``` ## First test Now, run the Console application. After the start, it waits for messages from the queue. The next step is to open the Admin UI and select **Queues and Streams**. Here, I see that the queue `testqueue` is created and ready to use. ![Queues and Streams in the RabbitMQ Management UI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-32.png?resize=640%2C246&ssl=1)Queues and Streams in the RabbitMQ Management UI Now, I click on the name of the queue and I see the details of it. In this page, I can see the utilisation of the queue, how many messages the queue manages, and more options. The option I am interested in now is **Publish Message**. ![RabbitMQ Queue testqueue](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-33.png?resize=640%2C565&ssl=1)RabbitMQ Queue **testqueue** You can see the section **Publish message** at the bottom of the screenshot above. It allows me to send a message. Now, type something in the Payload and press the button **Publish message**. Immediately, the message is been sent to the client. In the Console, I see the message. ![The Console application receives the message from the queue](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-34.png?resize=640%2C342&ssl=1)The Console application receives the message from the queue ## Connection issue As I said before, RabbitMQ is a service in your machine. For example, if you open the Services in Windows, you see the Windows service up and running. ![RabbitMQ in the Windows Services](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-36.png?resize=640%2C346&ssl=1)RabbitMQ in the Windows Services If I stop the service, RabbitMQ Client tries to reconnect to the server. It is not always successful. At some point, it won’t try again. So, how can I improve the connection settings and stabilize it? ## ConnectionShutdownAsync event The `connection` object has an event that I can subscribe to detect when a connection breaks. So, I’m going to refactor the code above in order to receive the event and then manage it. First, I’m going to declare the following variables at the top of the code: ``` ConnectionFactory factory; IConnection connection = null; IChannel channel = null; AsyncEventingBasicConsumer consumer; ``` After that, I’m going to organize better the `using` and split the code after that to be more manageable. ### The Connect function Now, the first function I want to create is the `Connect`. It is responsible for creating a connection with RabbitMQ. It also creates a channel to the `testqueue`. Additionally, it subscribes to 2 events: `ConnectionShutdownAsync` and `ReceivedAsync` for `AsyncEventingBasicConsumer`. Here is the final code: ``` async Task Connect() { connection = await factory.CreateConnectionAsync(); connection.ConnectionShutdownAsync += Connection_ConnectionShutdownAsync; channel = await connection.CreateChannelAsync(); await channel.QueueDeclareAsync("testqueue", true, false, false, null); consumer = new AsyncEventingBasicConsumer(channel); consumer.ReceivedAsync += Consumer_ReceivedAsync; await channel.BasicConsumeAsync("testqueue", true, consumer); } ``` With the code above, the `Consumer_ReceivedAsync` remains the same as before. What I need to create is the `Connection_ConnectionShutdownAsync`. ``` async Task Connection_ConnectionShutdownAsync(object sender, ShutdownEventArgs ev) { Console.WriteLine("Connection lost"); } ``` This is not completed yet. When the connection is lost, I have to clean the `channel`. I also clean the `connection` with RabbitMQ. Then, I establish a new one when the connection is up again. ### The CleanUp function So, now I want to create a function to clean the `channel` and the `connection`. When the connection is lost, I have to create a new connection with the channel. I also have to establish another connection. The code for this function is quite straightforward: ``` async Task CleanUp() { try { if (channel != null && channel.IsOpen) { await channel.CloseAsync(); channel = null; } if(connection != null && connection.IsOpen) { await connection.CloseAsync(); connection = null; } } catch(IOException ex) { // Close() may throw an IOException if connection dies // (handled by reconnect) } } ``` If the `channel` or the `connection` is open, I have to close them and restart. ## How to reconnect First, it’s important to say that when the application starts, and for some reason the connection with RabbitMQ doesn’t exist, the application is going to crash. I want to test the connection regularly until it is restored. For that I can use a simple `Thread.Sleep()` but this is not very efficient. So, I prefer to use something like [ManualResetEventSlim](https://msdn.microsoft.com/en-us/library/system.threading.manualreseteventslim(v=vs.110).aspx). A `ManualResetEventSlim` is like a semaphore, but only has on and off (Set and Reset) states. Although it is mostly useful in multi-threading scenarios, we can use it instead of `Thread.Sleep()` to periodically reconnect. ``` async Task Reconnect() { await CleanUp(); var mres = new ManualResetEvent(false); while(!mres.WaitOne(3000)) { try { await Connect(); Console.WriteLine("Connected"); mres.Set(); } catch(Exception ex) { Console.WriteLine("Connection failed"); } } } ``` So, what this function is now doing is to call the clean function to have reset the variables. Then, set to execute the code to 3000 milliseconds using `WaitOne`. In the `try ... catch`, I’m call the `Connect` function. If there is an error in this function, the `catch` returns a *Connection failed*. However, the `while` loop will run again until the connection is established. Now, I can update the `Connection_ConnectionShutdownAsync` function like that ``` async Task Connection_ConnectionShutdownAsync(object sender, ShutdownEventArgs ev) { Console.WriteLine("Connection lost"); await Reconnect(); } ``` ### Update the Main function Now, in the `Main` function, I have to call the `Reconnect` function and nothing else. The code is like that factory = new ConnectionFactory() { HostName = “localhost” }; await Reconnect(); ## Test the connection Now, I have to check if the connection is re-established after a loss. I also need to verify that the messages can be received by the application. I start the application. Then, using the Rabbit Management UI, I am going to send a message (*Test* in the following screenshot). Then, I stop the RabbitMQ service and wait the service is stopped. I see the application detects the lost. Now, I start the RabbitMQ service and the application detects the connection. Again, I stop and restart the service. When the connection is established again, I send a new test message. ![The RabbitMQ tests](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-37.png?resize=640%2C342&ssl=1)The RabbitMQ tests ## Wrap up I hope this post about creating a resilient connection to RabbitMQ helps you. This should enable you to manage the connection itself better. The code is available on [GitHub](https://github.com/erossini/RabbitMQConnection/). If you have any suggestions or questions, please keep in touch. **Categories:** .NET7, .NET8, .NET9 **Tags:** connection, queue, rabbitmq **Hashtags:** rabbitmq --- ### [Release SSIS using Azure DevOps](https://puresourcecode.com/dotnet/sql/release-ssis-using-azure-devops/) **Published:** May 25, 2025 **Author:** Enrico **Excerpt:** Steps on how to create a working pipeline to release SSIS packages using Azure DevOps from the creation of the artifact to the deployment. **Content:** With this new post, I continue to explain how to release SSIS packages using pipelines in [Azure DevOps](https://puresourcecode.com/tag/azure-devops/). This helps you achieve a proper CD/CI for your Windows Service projects. Some context is provided by the [Microsoft documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/windows-agent?view=azure-devops&tabs=IP-V4). Here is the list of posts related to this one: - [Deploying Windows Services using Azure DevOps](https://puresourcecode.com/tools/azure-devops/deploying-windows-services-using-azure-devops/) - [Releasing Windows Services using Azure DevOps](https://puresourcecode.com/tools/azure-devops/releasing-windows-services-using-azure-devops/) - [Release SSIS (SQL Server Integration Services) using Azure DevOps](https://puresourcecode.com/tools/azure-devops/release-ssis-using-azure-devops/) The source code of this post is available on [GitHub](https://github.com/erossini/WindowsServicePipeline). This post is part of the deployment posts I wrote before, and you can see the link above. I assume the connection with Azure DevOps and the target SQL machine is established using the script Azure DevOps provides. For more details, look at the [first post](https://puresourcecode.com/tools/azure-devops/deploying-windows-services-using-azure-devops/). ## Create the artifact for SSIS First, an SSIS (SQL Server Integration Services) package can be created in Visual Studio. You need to install [SQL Server Integration Services Projects 2022](https://marketplace.visualstudio.com/items?itemName=SSIS.MicrosoftDataToolsIntegrationServices) from the Visual Studio Marketplace. Then, the code for the SSIS package is stored in a repository in [Azure DevOps](https://puresourcecode.com/category/tools/azure-devops/). Now, we can begin the first part of the full pipeline. This involves creating the artifact of the SSIS package. So, start a new pipeline and select at the bottom of the page the option **Use classic editor**. ![Where is your code? - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-1.png?resize=640%2C534&ssl=1)Where is your code in a Azure DevOps pipeline? After that, you have to select the source from where you read the code. As I said, the repository in my case is in Azure DevOps. So, I select **Azure Repos Git**. ![Select a source - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image.png?resize=640%2C275&ssl=1)Select a source Now, select the **Team project**, the **Repository** and the **Default branch**. The branch can be changed later if it is needed. Then, click on the **Continue** button. Then, there is a pipeline template to choose from. I select **Empty job** because there is no one template for SSIS. ![Select a template for the pipeline - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-2.png?resize=640%2C354&ssl=1)Select a template for the pipeline After that, I am redirected to the page where I can design the pipeline. In the **Pipeline** section, select “**Azure Pipelines**” as the **Agent Pool** for the build pipeline. Also, in the **Agent Specification**, select **windows-2019**. ![Set up the Agent job - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-3.png?resize=640%2C274&ssl=1)Set up the Agent job Now, just for my reference, I have changed the **Display name** for the build agent job. This is not required but it helps you to understand what the pipeline does. ![Change the Display name of the Agent job - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-4.png?resize=640%2C226&ssl=1)Change the Display name of the Agent job The next step is to add a new task to the pipeline. For that, there is a `+` sign related to the agent. So, click on the `+` to add a new task. ![Add a task to the agent job](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-5.png?resize=640%2C258&ssl=1)Add a task to the agent job ### Add SSIS DevOps Tools Now, the tasks I have to add are part of the **SSIS DevOps Tools**, a free package offered by Microsoft. If this package is not installed in your instance of Azure DevOps, you need to install it. You can get it free from the marketplace. So, click on the button **Get it free** and authorize the installation. Here is the [link to this tool in the marketplace](https://marketplace.visualstudio.com/items?itemName=SSIS.ssis-devops-tools). ![Add SSIS DevOps Tools to your Azure DevOps organization - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-6.png?resize=640%2C269&ssl=1)Add SSIS DevOps Tools to your Azure DevOps organisation After the installation of the **SSIS DevOps Tools**, there are 3 new tasks in the list: - **SSIS Build Task**: supports building single or multiple dtproj files in project deployment model or package deployment model. - **SSIS Deploy Task**: supports deploying single or multiple ispac files to on-premise SSIS catalog and Azure-SSIS IR, or SSISDeploymentManifest files and their associated files to on-premise or Azure file share. - **SSIS Catalog Configuration Task**: supports configuring folder/project/environment of SSIS catalog with a configuration file in JSON format. ### Add the SSIS tasks Now, I can continue with the release of SSIS packages using Azure DevOps. So, I am going to add a new task using the task called **SSIS Build**. So, select **SSIS Build** from the list of tasks. ![Add SSIS Build to the Agent job - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-7.png?resize=640%2C270&ssl=1)Add SSIS Build to the Agent job After clicking on the task, I have to click on the button **Add**. ![Add the SSIS Build to the Agent job - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-8.png?resize=640%2C304&ssl=1)Add the SSIS Build to the Agent job Now, in the pipeline, I see the new task. This task needs to be configured now. ![The SSIS Build is ready to be configured - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-9.png?resize=640%2C268&ssl=1)The SSIS Build is ready to be configured So, click on the task **Build SSIS** and see the settings for this task. First, I have to select the **Project Path** to find the SSIS project to build. Click on the `...` to select the project to build. ![Select the Project Path - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-11.png?resize=640%2C246&ssl=1)Select the Project Path After clicking on the `...`, I have a pop-up window with the content of the repository I chose at the beginning. From this list, I have to choose the file with extension `dtproj`. ![Select path for Build SSIS - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-10.png?resize=640%2C389&ssl=1)Select path for Build SSIS After that, I have to add another task to **Publish build artifacts**. Again, click on the `+` in the Agent Job and in the list of tasks, search for *publish artifact*. Then, click on it to add it to the pipeline. ![Add the task Publish build artifacts - Release SSIS using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-12.png?resize=640%2C292&ssl=1)Add the task Publish build artifacts Now, the **Publish Artifact** task is added to the Agent Job. Click on it to see the settings. I am going to change the **Artifact name**. I found quite useful to select a specific name like *drop*, because in the Release deployment pipeline, I have to select the name of the artifact. If I select a random name or depending from variables like **BuildId**, it will prevent me to select an artifacts to deploy. ![Change the Artifact name](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-13.png?resize=640%2C281&ssl=1)Change the Artifact name After this change, I can click on **Save and run** the build pipeline. If everything is set correctly, the pipeline will run successfully. Here is an example of the result of my pipeline. ![Successful build](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-14.png?resize=640%2C251&ssl=1)Successful build ### Set the CD/CI for the pipeline I want to execute the pipeline now. A new artifact should be created every time there is a change in the branch. So, click on the tab **Triggers** and then click on the checkbox for **Enable continuous integration**. ![Enable continuous integration](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-15.png?resize=640%2C250&ssl=1)Enable continuous integration ## Release pipeline Now, here is the second part of the pipeline. So far, I have built the artifact, but now I have to deploy the artifact to the SQL Server. For that, click on the **Releases** under **Pipelines**. If this is the first pipeline release, you see a page like this one. ![New release pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-41.png?resize=640%2C311&ssl=1)New pipeline If you have already other pipelines, you can add a new release pipeline, click on the button **New** and then click on **New release pipeline**. ![New release pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-42.png?resize=640%2C202&ssl=1)New release pipeline Now, clicking on that, I see a page like the following page. From this page, click on **Empty job**. ![Empty job pipeline release](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-16.png?resize=640%2C227&ssl=1)Empty job pipeline release I want to deploy the SSIS package on different environments. Therefore, I will name every stage with the name of the environment. For example, the first stage is **DEV**. ![Stage on the release pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-17.png?resize=640%2C229&ssl=1)Stage on the release pipeline Now, I have to configure the artifact I want to deploy. For that, I click on **Add an artifact**. Remember the **drop** file I created before? ![Add the artifact to the release pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-18.png?resize=640%2C385&ssl=1)Add the artifact to the release pipeline Now, I have to select the source. Click on the dropdown list and select the artifact I want to deploy. ![Select the source](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-43.png?resize=640%2C221&ssl=1)Select the source After that, the other fields are automatically filled. Here is an example of the settings. ![Select the package](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-19.png?resize=640%2C341&ssl=1)Select the package Now, click on the button at the bottom with the **Add** text. Next, I am going to configure the tasks in the first stage. For that, click on the link under the name of the the stage to configure the stage. ![Set the tasks in a stage](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-20.png?resize=640%2C427&ssl=1)Set the tasks on a stage First, again, I am going to change the **Display name** of the Agent job. This is not necessary, but it helps me to understand what the pipeline is doing. ![Release Agent job](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-21.png?resize=640%2C290&ssl=1)Release Agent job Also, select from the **Agent pool** in the **Agent selection**, the **Default** option. Now, I have to add a new task for this Agent job. Click on the `+` to do so. ![Add a new task](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-22.png?resize=640%2C278&ssl=1)Add a new task The next step is to add the task called **SSIS Deploy** that is part of the **SSIS DevOps Tools**. If I move the mouse on this task, I see the option to **Add** the task to the agent. ![Add SSIS Deploy](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-23.png?resize=640%2C285&ssl=1)Add SSIS Deploy Now, I have to configure the task. So, I have to click on it. ![Configure Deploy SSIS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-24.png?resize=640%2C212&ssl=1)Configure Deploy SSIS The setting for this task are like in the following screenshot. ![SSIS Deploy configuration](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-45.png?resize=640%2C785&ssl=1)SSIS Deploy configuration Now, I have to select the **Source path** for the package I want to deploy to the SQL Server. For that, click on the `...` to open a pop-up window with the list of packages in the folder. Select a file with`.ispac` extension. ![Select Source Path](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-26.png?resize=640%2C413&ssl=1)Select Source Path After that, in the dropdown list for **Destination type**, choose **SSIS**. ### Add a variable for the SQL server name Now, because every environment has a different SQL Server name, I will add a variable for it. So, click on the tab **Variables**, add a variable for the server name and then set the scope to **DEV**. ![Add a variable for the DEV release](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-47.png?resize=640%2C155&ssl=1)Add a variable for the DEV release Go back to **Tasks** and then set the **Destination Server** to the variable `$(ServerName)`. Now, set the **Destination Path to the destination folder where the source file will be deployed**. For example: 1. /SSISDB//\[\] 2. \\\\\\\\ Set the **Authentication Type** to **Windows Authentication**. This is because I run the Azure DevOps script to connect the SQL machine to Azure DevOps. ![Settings of the Deploy SSIS task](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-28.png?resize=640%2C440&ssl=1)Settings of the Deploy SSIS task Set the **Authentication Type** to **Windows Authentication**. Now, the settings look like the following screenshot. ![Deploy SSIS settings](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-38.png?resize=640%2C571&ssl=1)Deploy SSIS settings You can rename the pipeline and then **Save**. ## Create a new release Now, the last action is to start the deployment. For that, I click the **Create release** at the top right of the screen. ![Create a release](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-40.png?resize=640%2C286&ssl=1)Create a release When I click, a drawer opens with the settings for this release. I can check the target environment, and I can add a description for this release. ![Create release settings](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/05/image-39.png?resize=640%2C978&ssl=1)Create release settings Now, everything is fine for me. So, I can click on the **Create** button. If the release pipeline is configured correctly, in the SQL Server I can see the package. ## Wrap up I hope this post can help you in setting a new release for SSIS packages using Azure DevOps. I have created many pipelines for that for different environments. Please keep in touch if you have any comments or questions about it. **Categories:** Azure DevOps, SQL **Tags:** azure-devops, azure-pipeline, microsoft-sqlserver, ssis **Hashtags:** azure-devops, azure-pipeline, ssis --- ### [Releasing Windows Services using Azure DevOps](https://puresourcecode.com/tools/windows/releasing-windows-services-using-azure-devops/) **Published:** April 22, 2025 **Author:** Enrico **Excerpt:** Here I explain how releasing Windows Services using pipelines in Azure DevOps. It helps you achieve a CD/CI for your Windows Service project **Content:** With this new post, I continue to explain how releasing and deploying Windows Services using pipelines in [Azure DevOps](https://puresourcecode.com/tag/azure-devops/). This helps you achieve a proper CD/CI for your Windows Service projects. Some context is provided by the [Microsoft documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/windows-agent?view=azure-devops&tabs=IP-V4). Here the list of posts related to this one: - [Deploying Windows Services using Azure DevOps](https://puresourcecode.com/tools/azure-devops/deploying-windows-services-using-azure-devops/) - [Releasing Windows Services using Azure DevOps](https://puresourcecode.com/tools/azure-devops/releasing-windows-services-using-azure-devops/) - [Release SSIS (SQL Server Integration Services) using Azure DevOps](https://puresourcecode.com/tools/azure-devops/release-ssis-using-azure-devops/) The source code of this post is available on [GitHub](https://github.com/erossini/WindowsServicePipeline). Windows Services built on [.NET Core](https://puresourcecode.com/category/dotnet/net-core/) and classic [.NET Framework](https://puresourcecode.com/category/dotnet/) can be deployed using Azure DevOps to our target machine(s). They can automatically run on these machines. This process removes the need to copy files manually. You will have the following ready: - An Azure DevOps account - Your working Windows Service code is committed in Azure DevOps Repositories - A target Windows machine to deploy to with an internet connection that you have access to. The target machine must have access to the URL `dev.azure.com` So, I assume that the code for the Windows Service is in a repository. The pipeline will build the service in this repository. It will then deploy it on the Windows Service machine. ## Configuring Windows Service Before starting with the creation of the Release pipeline, it is important to know `sc.exe`. This is a tool by Microsoft to manipulate the Windows Services. We can create, stop and remove services for example. The *sc.exe config* command is used to modify the configuration of a service in the Windows registry. It also modifies the Service Control Manager database. This command allows you to change various parameters of a service. You can modify its type, start mode, and error control. Additionally, you can adjust the binary path, dependencies, and more. The basic syntax of the *sc.exe config* command is as follows: ``` sc.exe [] config [] [type= {own | share | kernel | filesys | rec | adapt | interact type= {own | share}}] [start= {boot | system | auto | demand | disabled | delayed-auto}] [error= {normal | severe | critical | ignore}] [binpath= ] [group= ] [tag= {yes | no}] [depend= ] [obj= { | }] [displayname= ] [password= ] ``` Here is the list of parameters in detail: ParameterDescription``Specifies the name of the remote server on which the service is located. The name must use the Universal Naming Convention (UNC) format (for example, \\myserver). To run SC.exe locally, don’t use this parameter.``Specifies the service name returned by the **getkeyname** operation.`type= {own | share | kernel | filesys | rec | adapt | interact type= {own | share}}`Specifies the service type. The options include: **own** – Specifies a service that runs in its own process. It doesn’t share an executable file with other services. This is the default value. **share** – Specifies a service that runs as a shared process. It shares an executable file with other services. **kernel** – Specifies a driver. **filesys** – Specifies a file system driver.**rec** – Specifies a file system-recognized driver that identifies file systems used on the computer. **adapt** – Specifies an adapter driver that identifies hardware devices such as keyboards, mice, and disk drives. **interact** – Specifies a service that can interact with the desktop, receiving input from users. Interactive services must be run under the LocalSystem account. This type must be used in conjunction with **type= own** or **type= shared** (for example, **type= interact** **type= own**). Using **type= interact** by itself will generate an error.`start= {boot | system | auto | demand | disabled | delayed-auto}`Specifies the start type for the service. The options include: **boot** – Specifies a device driver that is loaded by the boot loader. **system** – Specifies a device driver that is started during kernel initialization. **auto** – Specifies a service that automatically starts each time the computer is restarted and runs even if no one logs on to the computer. **demand** – Specifies a service that must be started manually. This is the default value if **start=** is not specified. **disabled** – Specifies a service that cannot be started. To start a disabled service, change the start type to some other value. **delayed-auto** – Specifies a service that starts automatically a short time after other auto services are started.`error= {normal | severe | critical | ignore}`Specifies the severity of the error if the service fails to start when the computer is started. The options include: **normal** – Specifies that the error is logged and a message box is displayed, informing the user that a service has failed to start. Startup will continue. This is the default setting. **severe** – Specifies that the error is logged (if possible). The computer attempts to restart with the last-known good configuration. This could result in the computer being able to restart, but the service may still be unable to run. **critical** – Specifies that the error is logged (if possible). The computer attempts to restart with the last-known good configuration. If the last-known good configuration fails, startup also fails, and the boot process halts with a Stop error. **ignore** – Specifies that the error is logged and startup continues. No notification is given to the user beyond recording the error in the Event Log.`binpath= `Specifies a path to the service binary file. There is no default for **binpath=**, and this string must be supplied. Additionally, **ntsd -d** can be specified in front of the string for debugging. For more information, see [Debugging using CDB and NTSD](/en-us/windows-hardware/drivers/debugger/debugging-using-cdb-and-ntsd).`group= `Specifies the name of the group of which this service is a member. The list of groups is stored in the registry, in the **HKLM\\System\\CurrentControlSet\\Control\\ServiceGroupOrder** subkey. The default value is null.`tag= {yes | no}`Specifies whether or not to obtain a TagID from the CreateService call. Tags are used only for boot-start and system-start drivers.`depend= `Specifies the names of services or groups that must start before this service. The names are separated by forward slashes (/).`obj= { | }`Specifies a name of an account in which a service will run, or specifies a name of the Windows driver object in which the driver will run. The default setting is **LocalSystem**.`displayname= `Specifies a descriptive name for identifying the service in user interface programs. For example, the subkey name of one particular service is **wuauserv**, which has a more friendly display name of Automatic Updates.`password= `Specifies a password. This is required if an account other than the LocalSystem account is used./?Displays help at the command prompt.For more information about `sc.exe`, read the Microsoft documentation here: - - ## Create the Release pipeline So, in my [previous post](https://puresourcecode.com/tools/azure-devops/deploying-windows-services-using-azure-devops/), I created the pipeline to build the project and publish it in the artifacts. Now in Azure DevOps, go to **Pipelines** > **Releases** > **New** and then **New Release Pipeline**. ![Create a new Release pipeline - Releasing Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-20.png?resize=640%2C280&ssl=1)Create a new Release pipeline On the Template selector that shows up, choose **Empty Job** at the top. ![Create a new Release pipeline - Releasing Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-21.png?resize=640%2C393&ssl=1)Create a new Release pipeline Then give this step the name you like. I called mine **DEV**. This was before it would deploy on the developer machine. I closed the right-hand panel for this. Usually, the name of this stage should be the name of the environment you are deploying to. ![Create the Empty job in the Azure DevOps pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-22.png?resize=640%2C257&ssl=1)Create the Empty job in the Azure DevOps pipeline Next, click on **Add an artifact** and then select the source of your **Artifact**. In the following screenshot, I select the **Build** from the project in Azure DevOps. The the source is the pipeline of the project (in my case, the project is called `WindowsServiceTest`). That is now available from our **Azure Artifacts** that I built previously. ![Select the artifacts from the list](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-23.png?resize=640%2C307&ssl=1)Select the artifacts from the list Then, click on the **Add** button. For a CD/CI of this service, make sure to click the **Lightning icon** on the Artifact just added. Then **Enable Continuous deployment trigger** and **click Save then Click OK**. Doing this will mean that every time you commit code to your project Repository in **Azure Repos** and a Build completes successfully, a new Release is automatically created and triggered. ![Enable the continuous deployment trigger - Releasing Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-24.png?resize=640%2C253&ssl=1)Enable the continuous deployment trigger Now, we have to add the tasks for the real deployment. I’m going to split step by step all the tasks to add. From the tabs, click now **Tasks**. The screen is looking like the following screenshot. ![Tasks tab in the pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-25.png?resize=640%2C176&ssl=1)Tasks tab in the pipeline ## Add the tasks In the Tasks page we are now looking at, first click on **Agent Job.** Then click on **Remove** on the right-hand side to remove it. ![Remove Agent job](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-28.png?resize=640%2C202&ssl=1)Remove Agent job Now click on the 3 dots next to ‘DEV’ (or the name of your deployment process) and add a ***Deployment group job***. ![Add a deployment group job](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-27.png?resize=640%2C206&ssl=1)Add a deployment group job ### Add a Deployment group job After adding a new **Deployment group job**, a bit of configuration is required. In the settings of this option, select from the dropdown list of Deployment group, the name of the group to use for the deployment. In my case, it will be `Deployment machine`. ![Select the Deployment group](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-30.png?resize=640%2C391&ssl=1)Select the Deployment group This is how it looks after the selection of the **Deployment machine**. Now, I want to add a new task. For that, I have to click on the **+** related to the **Deployment group job**. ![Add a deployment group job](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-31.png?resize=640%2C392&ssl=1)Add a deployment group job ### Add Command line task to uninstall the service Now, the next step is to stop and remove the service if it already exists. I want to save all the Windows Services in a common directory. Each service should have its own directory within it. I called the common folder `ADOWindowsServices` (ADO = Azure DevOps). Remember that my service is called `WindowsServiceTest`. So, I want to check as follows: - stop the Windows service - delete the Windows service - verify if the folder for the service exists. If it does not exist, I can skip the steps up to `NODIR` - Enter in the `ADOWindowsServices` - Delete the folder To stop and delete the service, I use `sc.exe` ``` sc STOP sc DELETE ``` I found that this command is working better if the parameters are in capital letters. Maybe it is just me. Then, I want to check if the folder for the service exists already. The folder that contains all the other folders is located in the root of `C`:. ``` C: IF NOT EXIST C:\ADOWindowsServices\WindowsServiceTest GOTO NODIR ``` if the folder for the service is not exists, the script will jump to the label `NODIR`. If not, it opens the common folder and delete the service folder. ``` cd C:\ADOWindowsServices rd /s /q WindowsServiceTest ``` So, first, I have to add from the catalogue the **Command Line** task. So, click on the **+** in the **Deployment group job** tab and the list of tasks will appear. Search for this task and click **Add**. ![Add Command Line task](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-32.png?resize=640%2C391&ssl=1)Add Command Line task Then, click on the task to see the settings. Change the **Display name** and the **Script**. ![Command Line task settings](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-33.png?resize=640%2C318&ssl=1)Command Line task settings Although, it doesn’t seem allow to paste a YAML script, I can copy the commands from the script below and paste in the **Script**. This is the [YAML](https://puresourcecode.com/tools/what-is-yaml/) of this step. Remember to rename `WindowsServiceTest` with the name of your service. ``` steps: - script: | sc STOP WindowsServiceTest sc DELETE WindowsServiceTest C: IF NOT EXIST C:\ADOWindowsServices\WindowsServiceTest GOTO NODIR cd C:\ADOWindowsServices rd /s /q WindowsServiceTest :NODIR displayName: 'Uninstall Existing Service' ``` > ### Extract files Then, the next step is to extract the files from the zip file. Just as a reminder, in the [previous post](https://puresourcecode.com/tools/azure-devops/deploying-windows-services-using-azure-devops/), I created a zip file from the build folder. Here, I’m going to unzip the file in the service folder. So, click on the **+** in the **Deployment group job** tab and the list of tasks will appear. Search for **Extract files** and **Add** this task. ![Add Extract files to the Release pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-34.png?resize=640%2C393&ssl=1)Add Extract files to the Release pipeline Now, in the setting of this task, the **Destination folder** is required. ![Extract files destination folder](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-36.png?resize=640%2C315&ssl=1)Extract files destination folder In my case, the destination folder is `C:\ADOWindowsServices\WindowsServiceTest`. So, the setting result is ![Example of the destination folder](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-38.png?resize=640%2C302&ssl=1)Example of the destination folder ### Install the service Now, the last thing to do is to install and start the service. Again, I will use `sc.exe` to do that. Here is the script to copy and paste into the settings. Remember to change the name of the service to yours. ``` SC CREATE WindowsServiceTest start=auto binpath=C:\ADOWindowsServices\WindowsServiceTest\WindowsServiceTest.exe SC description "WindowsServiceTest" "This is a Windows Service from Azure DevOps" SC START WindowsServiceTest ``` > I noticed that having a space in `start= auto` is working better than `start=auto` Here is what it looks like. ![Install new service and save pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-39.png?resize=640%2C286&ssl=1)Install new service and save pipeline ### Save Last but not least, remember to **Save** the pipeline. Also, you can rename the `New release pipeline` with a more meaningful name. ## Create a release After all of that, finally, I can create my first release. After saving, the **Create release** is enabled. ![Create a release enabled](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-40.png?resize=640%2C299&ssl=1)Create a release enabled When I click on this button, I see the configuration of the release. ![Create a new release](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-41.png?resize=640%2C392&ssl=1)Create a new release After clicking **Create**, a message appears to communicate that the release has been created. ![Release created](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-44.png?resize=640%2C52&ssl=1)Release created If I click on the release name (in the above screenshot Release-5), I can see the progress of the deployment. At the end of the process, I can see a screen like that. ![Release has been deployed](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-45.png?resize=640%2C448&ssl=1)Release has been deployed In this screen, I can see all the details about the deployment, see the logs and redeploy this specific build. ## Wrap up I hope those 2 posts about Deploying and Releasing Windows Services using Azure DevOps are useful. Please let me know if you have any further questions or comments. **Categories:** Azure DevOps, Windows **Tags:** azure-devops, azure-pipeline, pipeline, windows-service **Hashtags:** azure-devops, azure-pipeline, pipeline --- ### [Deploying Windows Services using Azure DevOps](https://puresourcecode.com/tools/windows/deploying-windows-services-using-azure-devops/) **Published:** April 22, 2025 **Author:** Enrico **Excerpt:** Here how deploying Windows Services using pipelines in Azure DevOps. This helps you achieve a proper CD/CI for your Windows Service projects **Content:** In this new post, I explain how deploying Windows Services using pipelines in [Azure DevOps](https://puresourcecode.com/tag/azure-devops/). This helps you achieve a proper CD/CI for your Windows Service projects. Some context is provided by the [Microsoft documentation](https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/windows-agent?view=azure-devops&tabs=IP-V4). Here the list of posts related to this one: - [Deploying Windows Services using Azure DevOps](https://puresourcecode.com/tools/azure-devops/deploying-windows-services-using-azure-devops/) - [Releasing Windows Services using Azure DevOps](https://puresourcecode.com/tools/azure-devops/releasing-windows-services-using-azure-devops/) - [Release SSIS (SQL Server Integration Services) using Azure DevOps](https://puresourcecode.com/tools/azure-devops/release-ssis-using-azure-devops/) The source code of this post is available on [GitHub](https://github.com/erossini/WindowsServicePipeline). Windows Services built on [.NET Core](https://puresourcecode.com/category/dotnet/net-core/) and classic [.NET Framework](https://puresourcecode.com/category/dotnet/) can be deployed using Azure DevOps to our target machine(s). They can automatically run on these machines. This process removes the need to copy files manually. You will have the following ready: - An Azure DevOps account - Your working Windows Service code is committed in Azure DevOps Repositories - A target Windows machine to deploy to with an internet connection that you have access to. The target machine must have access to the URL `dev.azure.com` So, I assume that the code for the Windows Service is in a repository and the pipeline will build the service in this repository and deploy it on the Windows Service machine. ## Create a build pipeline First, I created in my Azure DevOps a project called **WindowsServiceTest** and here I’m going to create the pipeline. Now, on the menu on the left select **Pipelines** and **Create Pipeline**. ![Create your first pipeline in Azure DevOps - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-1.png?resize=640%2C282&ssl=1)Create your first pipeline in Azure DevOps After clicking on the create button, I can select where the code is. As I said above, the code is in Azure DevOps. So, I choose **Azure Repos Git**. In the following screenshot an example where to click. ![Select Where is your code? - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image.png?resize=640%2C414&ssl=1)Select Where is your code? After that, I have to select the repository. In my case, this is straightforward because I don’t have any other repository apart of the main one. ![Select a repository](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-2.png?resize=640%2C413&ssl=1)Select a repository Now, Azure DevOps asks if I want to start with a new pipeline (**Starter pipeline**) or using an existing one. ![Configure your pipeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-3.png?resize=640%2C375&ssl=1)Configure your pipeline Click on the button **Show more** to display a list of other pipelines. I can see this screen now. ![Configure your pipeline: .NET Desktop](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-4.png?resize=640%2C512&ssl=1)Configure your pipeline: .NET Desktop Now, from the list, select **.NET Desktop**. This boilerplate offers us some useful settings. This will be relevant for both .NET Core and .NET Framework. This is the [YAML](https://puresourcecode.com/tools/what-is-yaml/) out-of-the-box. ``` # .NET Desktop # Build and run tests for .NET Desktop or Windows classic desktop solutions. # Add steps that publish symbols, save build artifacts, and more: # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net trigger: - main pool: vmImage: 'windows-latest' variables: solution: '**/*.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' steps: - task: NuGetToolInstaller@1 - task: NuGetCommand@2 inputs: restoreSolution: '$(solution)' - task: VSBuild@1 inputs: solution: '$(solution)' platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' - task: VSTest@2 inputs: platform: '$(buildPlatform)' configuration: '$(buildConfiguration)' ``` ![Review your pipeline YAML - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-5.png?resize=640%2C552&ssl=1)Review your pipeline YAML ## Customize the pipeline Before showing the pipeline, a few consideration. I have some **NuGet packages** stored in the **Azure DevOps Artifacts**. I want to restore those packages from my **artifacts feed**. Also, my priority is to run tests and show the **Tests** result and the **Code coverage**. ### General settings First, the general settings in the YAML. The **trigger** is from the main branch. The image I want to use is the `windows-latest`. As a `variables`, I set the build as a `Release`. ``` trigger: - main pool: vmImage: windows-latest variables: buildConfiguration: 'Release' ``` After that, I will add the **steps**. Now, I explain step by step the tasks I want to add. ### Install .NET Now, I built the Windows Service with the version 8 of NET Core. So, the first task is to install it. ``` - task: UseDotNet@2 displayName: 'Use dotnet 8' inputs: version: '8.0.x' ``` ### Use NuGet As I said before, some of the packages are in the artifacts, I added a `Nuget.config` in the root of the project. The tasks I’m adding are related to install and use the **NuGet tool** in the pipeline. After that, I list the NuGet sources and then restore the packages for the solution. ``` - task: NuGetToolInstaller@1 - task: NuGetAuthenticate@1 displayName: 'Authenticate to Azure Artifacts feed' - script: dotnet nuget list source displayName: 'List NuGet sources' - task: NuGetCommand@2 displayName: 'NuGet Restore with custom config' inputs: restoreSolution: '**/*.sln' feedsToUse: config nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config' ``` This is an example of the `NuGet.config` I use for the project. This file must be in the root of the project. ``` ``` ### Build the project Now, the next step is to build the projects. The output of the build will be save/copy in the directory `ci-build` in the `StagingDirectory` of Azure DevOps. I decided to save the build there. This provides a clear place where I can find all the files. Then, compress them for the next deployment. ``` - task: DotNetCoreCLI@2 displayName: Build project inputs: command: 'build' projects: '**/*.csproj' arguments: '--configuration $(buildConfiguration) -o $(Build.StagingDirectory)/ci-build' ``` ### Run the tests All my projects have tests. So, before continuing with the deployment, I want to be sure that all the tests passed. After that, I want to publish the collect the results of the tests and the code coverage. ``` - task: DotNetCoreCLI@2 displayName: 'Run tests' inputs: command: 'test' projects: '**/*[Te]ests/*.csproj' arguments: '--configuration $(buildConfiguration) --collect "Code Coverage" --collect "XPlat Code Coverage"' ``` ### Publish the code coverage Once, the tests are completed and passed and the code coverage determined, I will publish the result. ``` - task: PublishCodeCoverageResults@2 displayName: 'Publish code coverage' inputs: summaryFileLocation: '$(Agent.BuildDirectory)/**/coverage.cobertura.xml' pathToSources: '$(Agent.BuildDirectory)/**/coverage' ``` Therefore, open an executed pipeline, I can see there are some new tabs: **Tests** and **Code Coverage**. ![Show the test results and code coverage in the pipeline - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-6.png?resize=640%2C197&ssl=1)Show the test results and code coverage in the pipeline Under the tab **Tests**, I see all the tests and if they passed or not. ![Azure DevOps: test result in the pipeline - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-8.png?resize=640%2C449&ssl=1)Azure DevOps: test result in the pipeline The **Code Coverage** tab shown how much code is covered by tests. Generally speaking, the code coverage should be around **77%** to get a good coverage of the projects. ![Azure DevOps: code coverage in the pipeline - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-10.png?resize=640%2C442&ssl=1)Azure DevOps: code coverage in the pipeline ### Zip the files Now that the build is done, I want to zip the files in the `ci-build` folder. The resulted zip file will be publish in the artifacts to be deploy later in the machine. ``` - task: ArchiveFiles@2 inputs: rootFolderOrFile: '$(Build.StagingDirectory)/ci-build' includeRootFolder: false archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' replaceExistingArchive: true ``` So, the configuration I use create a zip file and the name is the `BuildId` (a number). I don’t want to include the root of the folder but only the content of the folder. The file must be a **zip** file format. If the zip file exists, it can be replaced. ### Publish artifacts Finally, I want to publish the zip file in the artifacts. With that, the release will be use this artifact to deploy the Windows Service into the Windows Server machine or any other target machines. This will allow us for deploying Windows Services using Azure DevOps. ``` - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' publishLocation: 'Container' ``` ### Full YAML ``` trigger: - main pool: vmImage: windows-latest variables: buildConfiguration: 'Release' steps: - task: UseDotNet@2 displayName: 'Use dotnet 8' inputs: version: '8.0.x' - task: NuGetToolInstaller@1 - task: NuGetAuthenticate@1 displayName: 'Authenticate to Azure Artifacts feed' - script: dotnet nuget list source displayName: 'List NuGet sources' - task: NuGetCommand@2 displayName: 'NuGet Restore with custom config' inputs: restoreSolution: '**/*.sln' feedsToUse: config nugetConfigPath: '$(Build.SourcesDirectory)/NuGet.config' - task: DotNetCoreCLI@2 displayName: Build project inputs: command: 'build' projects: '**/*.csproj' arguments: '--configuration $(buildConfiguration) -o $(Build.StagingDirectory)/ci-build' - task: DotNetCoreCLI@2 displayName: 'Run tests' inputs: command: 'test' projects: '**/*[Te]ests/*.csproj' arguments: '--configuration $(buildConfiguration) --collect "Code Coverage" --collect "XPlat Code Coverage"' - task: PublishCodeCoverageResults@2 displayName: 'Publish code coverage' inputs: summaryFileLocation: '$(Agent.BuildDirectory)/**/coverage.cobertura.xml' pathToSources: '$(Agent.BuildDirectory)/**/coverage' - task: ArchiveFiles@2 inputs: rootFolderOrFile: '$(Build.StagingDirectory)/ci-build' includeRootFolder: false archiveType: 'zip' archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip' replaceExistingArchive: true - task: PublishBuildArtifacts@1 inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' ArtifactName: 'drop' publishLocation: 'Container' ``` ## Set the Deployment group Now, to deploy the services on the Windows Servers or any other machine, I need to specify our deployment target. Go to **Deployment Groups** and then **New** or **Add a deployment group**. ![Deployment group in Azure DevOps - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-11.png?resize=640%2C253&ssl=1)Deployment group in Azure DevOps Give your Deployment group a Name and description and click **Create**: ![New deployment group on Azure DevOps - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-12.png?resize=640%2C470&ssl=1)New deployment group on Azure DevOps On the next screen, Azure DevOps gives me the PowerShell script I have to execute on the target machines. This will associate the machine with this group and I can use them later in the **Releases**. **Click on Use a Personal access token**, then Click Copy to clipboard: ![Deployment groups script to run - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-13.png?resize=640%2C304&ssl=1)Deployment groups script to run Now go to your **target** machine, Open an Administrator-privileged **Powershell** command prompt, paste your script and then execute. This will take a while, maybe 2 to 5 minutes. You should see something like the following if all is well, indicating that the Azure Pipelines agent is installed on the machine correctly: ![Script in execution - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-15.png?resize=572%2C421&ssl=1)Script in execution During the configuration, there are some questions. I use the default values. - **Enter deployment group tags for agent? (Y/N) (press enter for N)**: I pressed Enter - **Enter enable SERVICE\_SID\_TYPE\_UNRESTRICTED for agent service (Y/N) (press enter for N)**: I pressed Y - **Enter User account to use for the service (press enter for NT AUTHORITY/SYSTEM)**: I pressed Enter - **Enter whether to prevent service starting immediately after configuration is finished? (Y/N)** (press enter for N): I pressed Enter So, now the configuration is completed. This is a example of what I can see after that. ![Setup completed - Deploying Windows Services using Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-17.png?resize=572%2C421&ssl=1)Setup completed Now, in the Deployment groups in Azure DevOps, I see the Target machine group. It has 1 machine. The machine is online. ![Deployment groups in Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/04/image-18.png?resize=640%2C86&ssl=1)Deployment groups in Azure DevOps ## Wrap up In this first post about deploying Windows Services using Azure DevOps, I showed how to create the pipeline. This pipeline builds the solution of a Windows Server. Additionally, I demonstrated how to publish the artifact. In the next post, I will show how to create the release to deploy and run the Windows Server. Happy coding! **Categories:** Azure DevOps, Windows **Tags:** azure-devops, azure-pipeline, pipeline, windows-service **Hashtags:** azure-devops, pipeline --- ### [From Installation to Advanced Configuration: Setting Up Serilog for C# Success](https://puresourcecode.com/dotnet/net7/serilog-setup/) **Published:** May 11, 2025 **Author:** Enrico **Content:** In the ever-evolving landscape of software development, effective logging is crucial for maintaining robust and reliable applications. [Serilog](https://serilog.net/) has emerged as a leading tool for C# developers, offering a flexible and powerful logging solution tailored to modern needs. This comprehensive guide will walk you through the installation and advanced configuration of Serilog in your C# projects, ensuring that you harness its full potential. From setting up various sinks for diverse output channels to integrating custom logging classes that capture detailed runtime data, you’ll discover best practices for enhancing application maintainability and error tracking. Join us as we delve into the art of Serilog logging, transforming your approach to logging with expert techniques and insights. ## Introduction to Serilog Logging Serilog has become a cornerstone for C# developers seeking robust logging solutions. This section explores the reasons behind Serilog’s popularity, the benefits of comprehensive logging, and an overview of Serilog’s key features. ### Why Choose Serilog for C#? Serilog stands out as a premier logging framework for C# developers due to its flexibility and powerful features. Its structured logging approach allows for more meaningful and searchable log entries, making it easier to diagnose issues in complex applications. Serilog’s extensibility is another key advantage. With a wide array of available sinks, developers can easily direct logs to various outputs such as files, databases, or cloud services. Moreover, Serilog’s performance is optimized for high-throughput scenarios, ensuring that logging doesn’t become a bottleneck in your application. This combination of features makes Serilog an excellent choice for projects of all sizes. ### Benefits of Robust Logging Solutions Implementing a robust logging solution like Serilog offers numerous benefits to development teams and operations staff alike. Effective logging provides invaluable insights into application behavior, facilitating faster troubleshooting and debugging. With detailed logs, developers can trace the flow of execution through an application, identifying bottlenecks and optimizing performance. This level of visibility is crucial for maintaining and improving complex systems over time. Furthermore, comprehensive logging supports better security practices by enabling the detection and investigation of potential breaches or unauthorized access attempts. It also aids in compliance with various regulatory requirements that mandate detailed record-keeping of system activities. ### Understanding Serilog’s Core Features Serilog’s core features set it apart as a powerful logging framework. At its heart is the concept of structured logging, which allows for the creation of semantic, queryable log events rather than simple text messages. One of Serilog’s standout features is its use of message templates. These allow developers to create log messages with placeholders for data, which Serilog then fills with the provided values. This approach maintains the structure of the log data, making it easier to search and analyze later. Another key feature is Serilog’s extensive sink system. Sinks in Serilog are output destinations for log events, and [Serilog offers a wide variety of sinks](https://github.com/serilog/serilog/wiki/configuration-basics) to suit different needs, from simple console output to complex cloud-based analytics platforms. ## Setting Up Serilog in C# Getting started with Serilog in your C# project is straightforward. This section covers the initial setup process, including installation, basic configuration, and integration into your application. ### Installation and Basic Configuration To begin using Serilog in your C# project, you’ll need to install the necessary NuGet packages. The process is straightforward and can be done through the NuGet Package Manager or the command line. 1. Open your project in Visual Studio or your preferred IDE. 2. Access the NuGet Package Manager. 3. Search for and install the *Serilog* package and any additional sink packages you plan to use. Once installed, you can set up a basic configuration in your application’s entry point. This typically involves creating a *LoggerConfiguration* object and specifying your desired log levels and sinks. A simple configuration might look like this: ``` Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .WriteTo.Console() .CreateLogger(); ``` ### Integrating Serilog in Your Application After setting up the basic configuration, the next step is to integrate Serilog throughout your application. This involves replacing traditional logging calls with Serilog’s methods. Instead of using *Console.WriteLine()* or similar methods, you’ll use Serilog’s *Log.Information()*, *Log.Warning()*, *Log.Error()*, and other level-specific methods. These methods accept message templates and structured data. For example: ``` Log.Information("User {UserId} logged in from {IpAddress}", userId, ipAddress); ``` It’s also important to properly shut down Serilog when your application closes. This ensures that all buffered log events are written and resources are released: ``` Log.CloseAndFlush(); ``` ### Setting Up Essential Serilog Sinks Sinks are a crucial part of Serilog’s architecture, determining where your log events are sent. Setting up the right combination of sinks is key to an effective logging strategy. Common sinks include: - Console sink for immediate feedback during development - File sink for persistent logs - Database sinks for structured storage and querying - Cloud-based sinks for centralized logging in distributed systems To set up multiple sinks, you can chain them in your configuration: ``` Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day) .WriteTo.SQLite("logs.db") .CreateLogger(); ``` This configuration writes logs to the console, a daily rolling file, and a SQLite database, providing versatility in how you can access and analyze your logs. ## Advanced Serilog Configuration As your logging needs grow more sophisticated, Serilog offers advanced configuration options to meet complex requirements. This section delves into customizing log formats, implementing structured logging, and fine-tuning sink configurations. ### Customizing Log Output Formats Serilog provides extensive options for customizing the format of your log outputs. This flexibility allows you to tailor your logs to specific requirements or preferences. You can use the *OutputTemplate* property to define a custom format for your log messages. This template can include various properties of the log event, such as timestamp, log level, and message. For example: ``` .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}") ``` Additionally, Serilog supports JSON formatting out of the box, which is particularly useful for machine-readable logs: ``` .WriteTo.File(new JsonFormatter(), "log.json") ``` These customizations allow you to balance human readability with machine parseability, depending on your specific use case. ### Implementing Structured Logging Structured logging is a powerful feature of Serilog that allows you to include semantic information in your log events. This approach makes logs more searchable and analyzable, especially when dealing with large volumes of data. To implement structured logging, you use message templates with named placeholders: ``` Log.Information("Order {OrderId} created for {CustomerId}", orderId, customerId); ``` This method allows Serilog to capture not just the formatted message, but also the individual property values. When using a sink that supports structured data (like Elasticsearch or Seq), you can easily search and filter on these properties. For complex objects, you can use the *@* operator to tell Serilog to capture the entire object structure: ``` Log.Information("Created {@Order}", order); ``` This approach provides rich, queryable log data that can significantly enhance your ability to understand and debug your application’s behavior. ### Advanced Sink Configurations Serilog’s sinks can be configured with advanced options to meet specific logging requirements. These configurations can help you manage log storage, implement log rotation, and control log verbosity. For file sinks, you can implement log rotation to manage file sizes: ``` .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, fileSizeLimitBytes: 1073741824) ``` This configuration creates a new log file daily, keeps only the last 7 days of logs, and limits each file to 1GB. For database sinks, you can customize the table structure and batch insert operations for better performance: ``` .WriteTo.MSSqlServer( connectionString: "YourConnectionString", tableName: "Logs", autoCreateSqlTable: true, batchPostingLimit: 50, period: TimeSpan.FromSeconds(5)) ``` These advanced configurations allow you to fine-tune your logging setup to match your specific performance and storage requirements. ## Creating a Custom Logging Class Implementing a custom logging class can significantly enhance the maintainability and flexibility of your logging solution. This section explores best practices for designing such a class, capturing essential runtime information, and adhering to C# logging best practices. ### Designing for Maintainability When creating a custom logging class, the primary goal should be to enhance maintainability while providing a consistent interface for logging throughout your application. Start by encapsulating Serilog’s functionality within your custom class. This abstraction allows you to change the underlying logging implementation without affecting the rest of your codebase. Consider implementing a static class or a singleton pattern to ensure a single point of access for logging: ``` public static class Logger { private static readonly ILogger _logger = new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); public static void LogInformation(string message) => _logger.Information(message); public static void LogWarning(string message) => _logger.Warning(message); public static void LogError(string message) => _logger.Error(message); } ``` This approach provides a clean, maintainable interface for logging throughout your application. ### Capturing Essential Runtime Information A well-designed custom logging class should capture essential runtime information automatically, enriching your logs with valuable context. Implement methods that automatically include information such as the current user, session ID, or environment details: ``` public static void LogWithContext(string message, LogEventLevel level) { var enrichedLogger = _logger .ForContext("User", CurrentUser) "SessionId", CurrentSessionId) .ForContext("Environment", CurrentEnvironment); enrichedLogger.Write(level, message); } ``` This method ensures that every log entry includes crucial contextual information, making it easier to trace issues and understand the state of your application at the time of logging. ### Best Practices for C# Logging Adhering to best practices ensures that your custom logging class remains effective and efficient. Here are some key principles to follow: - Use log levels appropriately: Ensure that your class provides methods for different log levels (Debug, Information, Warning, Error, Fatal) and use them consistently. - Avoid logging sensitive information: Implement safeguards to prevent logging of passwords, personal data, or other sensitive information. - Include exception details: When logging errors, capture full exception details, including stack traces. - Use structured logging: Leverage Serilog’s structured logging capabilities in your custom class to make logs more searchable and analyzable. - Implement performance considerations: Use asynchronous logging for non-critical logs to avoid impacting application performance. By following these best practices, your custom logging class will provide a robust, maintainable, and effective logging solution for your C# applications. ## Enhancing Logs with Additional Information Enriching your logs with detailed contextual information can significantly improve their value for debugging and analysis. This section focuses on including function names and correlation IDs, logging JSON data and API errors, and ensuring comprehensive error tracking. ### Including Function Names and Correlation IDs Adding function names and correlation IDs to your logs provides crucial context for tracing application flow and connecting related events across distributed systems. To include function names, you can use C#’s *CallerMemberName* attribute: ``` public static void LogWithCaller(string message, [CallerMemberName] string callerName = "") { _logger.Information("{CallerName}: {Message}", callerName, message); } ``` For correlation IDs, consider implementing a middleware or filter that generates and attaches a unique ID to each request: ``` public class CorrelationIdMiddleware { private readonly RequestDelegate _next; public CorrelationIdMiddleware(RequestDelegate next) { _next = next; } public async Task InvokeAsync(HttpContext context) { string correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() ?? Guid.NewGuid().ToString(); context.Items["CorrelationId"] = correlationId; using (LogContext.PushProperty("CorrelationId", correlationId)) { await _next(context); } } } ``` This approach ensures that every log entry within a request includes the correlation ID, facilitating end-to-end tracing of requests through your system. ### Logging JSON Data and API Errors When working with APIs and JSON data, it’s crucial to log this information in a structured, easily queryable format. Serilog’s support for structured logging makes this task straightforward. For logging JSON data: ``` public static void LogJsonData(string message, object jsonData) { _logger.Information("{Message}: {@JsonData}", message, jsonData); } ``` The *`@`* operator tells Serilog to serialize the object, preserving its structure. For API errors, create a dedicated method that captures all relevant details: ``` public static void LogApiError(string endpoint, int statusCode, string responseBody) { _logger.Error("API Error: {Endpoint} returned {StatusCode}. Response: {ResponseBody}", endpoint, statusCode, responseBody); } ``` This method ensures that all necessary information about API errors is consistently logged, facilitating easier troubleshooting of integration issues. ### Ensuring Comprehensive Error Tracking Comprehensive error tracking is essential for maintaining robust applications. Your logging strategy should capture not just the error message, but also the full context in which the error occurred. Implement a method for logging exceptions that captures the stack trace and any inner exceptions: ``` public static void LogException(Exception ex, string contextMessage = "") { _logger.Error(ex, "{ContextMessage} Exception occurred: {ExceptionMessage}", contextMessage, ex.Message); } ``` Consider adding additional context such as the current user, the operation being performed, or any relevant application state: ``` public static void LogExceptionWithContext(Exception ex, string operation, object contextData) { _logger.Error(ex, "Exception during {Operation}. Context: {@ContextData}", operation, contextData); } ``` By ensuring that your logs capture comprehensive error information, you’ll be better equipped to quickly identify, understand, and resolve issues in your application. ### Extracting Logs with Specific Correlation IDs from Your Database When dealing with vast amounts of log data, especially in a database, it’s essential to have efficient ways to filter and extract relevant information such as logs associated with a specific correlation ID. Here’s how you can achieve this with Serilog: #### Ensure Logs are Stored with Correlation IDs As previously described, you should be capturing and storing correlation IDs in your logs. This is typically done by injecting a correlation ID into each request and including it in every log entry. #### Query logs Once your logs are structured and stored in a database, retrieving logs associated with a specific correlation ID can be straightforward. Here’s an example of how you might perform such a query in SQL: ``` SELECT * FROM Logs WHERE CorrelationId = 'your-specific-correlation-id' ORDER BY Timestamp ASC; ``` Replace *‘your-specific-correlation-id’* with the actual ID you wish to query. This SQL statement assumes that your logs are stored in a table named *Logs* and that each entry includes a *CorrelationId* field. **Using Serilog Sinks with Query Capabilities:** if using a more sophisticated setup where logs are stored in systems like Elasticsearch or Seq, you can leverage their query capabilities to extract data efficiently. For instance, in Elasticsearch, you might use: ``` { "query": { "term": { "CorrelationId.keyword": "your-specific-correlation-id" } } } ``` Automating Log Retrieval For automation, consider using a C# method or script to execute these queries and fetch logs programmatically. Here’s a basic example using Dapper in C#: ``` using (var connection = new SqlConnection("YourConnectionString")) { var sql = "SELECT * FROM Logs WHERE CorrelationId = @CorrelationId ORDER BY Timestamp ASC"; var logs = connection.Query(sql, new { CorrelationId = "your-specific-correlation-id" }); foreach (var log in logs) { Console.WriteLine(log.Message); } } ``` This approach ensures that you can trace and diagnose issues effectively by following the path of a specific correlation ID through your application’s logs. Adjust the queries according to your database schema and storage preferences. **Categories:** .NET7 --- ### [Microsoft Muse explained](https://puresourcecode.com/news/microsoft/microsoft-muse-explained/) **Published:** February 24, 2025 **Author:** Enrico **Excerpt:** Today, Microsoft announced a new generative AI model for games called Muse in Cambridge, UK. This new technology is explained here. **Content:** Today, Microsoft announced a new generative AI model for games called Muse in Cambridge, UK. This new technology is explained here. ## What is Microsoft Muse? Microsoft Muse is a groundbreaking generative AI model designed for gameplay ideation. Developed by Microsoft Research in collaboration with Xbox Game Studios’ Ninja Theory, Muse is the first World and Human Action Model (WHAM). It can generate game visuals, controller actions, or both, making it a powerful tool for game developers. ![Microsoft Muse explained](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-17.png?resize=534%2C300&ssl=1)Microsoft Muse explained Muse’s capabilities include creating complex gameplay sequences that are consistent over several minutes, predicting how a game will evolve from an initial prompt sequence, and understanding the 3D game world, including game physics and player actions. This allows developers to rapidly iterate, remix, and create immersive environments, opening up new possibilities for game design and creativity. You can learn more about Muse and its applications in gaming [here](https://www.microsoft.com/en-us/research/blog/introducing-muse-our-first-generative-ai-model-designed-for-gameplay-ideation/). ## How can Muse be used in game design? Muse can revolutionize the game design process in several exciting ways: 1. **Game World Creation:** Muse can generate detailed and immersive game environments, helping designers bring their visions to life quickly and efficiently. This includes everything from landscapes and cityscapes to interior settings and fantastical worlds. 2. **Character Actions and Behaviors:** Muse can simulate and suggest complex character actions and behaviors, allowing designers to create more dynamic and realistic gameplay. It can predict how characters will react in various scenarios, making it easier to develop engaging storylines and interactions. 3. **Gameplay Sequences:** Muse can create coherent and intricate gameplay sequences that unfold over several minutes. This helps designers test and iterate on game mechanics, ensuring a smooth and enjoyable player experience. 4. **Rapid Prototyping:** With Muse, designers can rapidly prototype different game ideas and concepts. This accelerates the development process and allows for more experimentation and innovation. 5. **Interactive Feedback:** Muse can provide real-time feedback on game design choices, helping designers make informed decisions and improve their games’ overall quality. 6. **Enhanced Creativity:** By handling some of the more repetitive and time-consuming tasks, Muse frees up designers to focus on the creative aspects of game design, pushing the boundaries of what’s possible in gaming. ## What are some practical steps to use Muse in game design? Using Muse in game design can greatly enhance your creativity and efficiency. Here are some practical steps to get you started: 1. **Set Up Muse:** Begin by integrating Muse into your game development environment. This may involve installing necessary software, plugins, or APIs. Ensure your team is familiar with Muse’s capabilities and features. 2. **Define Your Objectives:** Clearly outline what you want to achieve with Muse. Are you looking to generate game worlds, character actions, gameplay sequences, or something else? Having specific goals will help you make the most of Muse’s potential. 3. **Provide Input Data:** Muse requires initial input to generate content. This could be in the form of sketches, descriptions, existing game assets, or even rough gameplay sequences. The more detailed and specific your input, the better the output. 4. **Generate Content:** Use Muse to generate the desired content based on your input. This could be new game environments, character behaviors, or gameplay scenarios. Review the generated content and make any necessary adjustments. 5. **Iterate and Refine:** Muse allows for rapid prototyping, so take advantage of this by iterating on your designs. Generate multiple versions, test them in your game, and refine the content based on feedback and observations. 6. **Collaborate with Team:** Share the generated content with your team for feedback and collaboration. Use Muse to explore new ideas and push the boundaries of your game design. 7. **Test and Validate:** Thoroughly test the generated content within your game to ensure it meets your design and gameplay standards. Make any necessary adjustments to enhance the player experience. 8. **Deploy and Enjoy:** Once you’re satisfied with the content generated by Muse, integrate it into your final game build. Enjoy the creative possibilities that Muse has unlocked in your game design process. Leveraging Muse can streamline your workflow and elevate your game design to new heights. **Categories:** Microsoft **Tags:** games, microsoft, microsoft-muse, retro-games **Hashtags:** games, microsoft, microsoft-muse --- ### [APIs with Entity Framework Core: PUT](https://puresourcecode.com/dotnet/csharp/apis-with-entity-framework-core-put/) **Published:** February 23, 2025 **Author:** Enrico **Excerpt:** Continue the tutorial about APIs with Entity Framework Core and in particular how to implemente the PUT to update a record with dependencies. **Content:** Continuing the topic, I want to give a complete example of minimal APIs in Blazor with Entity Framework Core with complex objects.I always struggle to have a solution working when my model has dependencies with other object. Here I show my test and my code. The code is in [NET9](https://puresourcecode.com/category/dotnet/net9/). In the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/update-related-data?view=aspnetcore-9.0), there are some examples, but it is not complex enough. A few days ago, [I posted about another problem](https://puresourcecode.com/dotnet/net9/pendingmodelchangeswarning-with-net9/) I had with NET9 and Entity Framework Core. The full source code of this post is available on GitHub. If you have any questions, please comment below or post in the [forum](https://puresourcecode.com/forum/). The complete code is spanned through those posts: - [APIs with Entity Framework Core](https://puresourcecode.com/dotnet/net9/apis-with-entity-framework-core/) - [APIs with Entity Framework Core: POST](https://puresourcecode.com/dotnet/csharp/apis-with-entity-framework-core-post/) - [APIs with Entity Framework Core: PUT](https://puresourcecode.com/dotnet/net7/apis-with-entity-framework-core-put/) This is the most difficult part of the implementation. There are a few steps to do to avoid errors and get the code working. ## The initial code First, the code created by the **Scaffolded Item** in Visual Studio has generated this code for the **PUT** verb in the minimal API: ``` group.MapPut("/{id}", async Task (long id, Domain.Client client, MyDbContext db) => { var affected = await db.Clients .Where(model => model.Id == id) .ExecuteUpdateAsync(setters => setters .SetProperty(m => m.Id, client.Id) .SetProperty(m => m.FirstName, client.FirstName) .SetProperty(m => m.LastName, client.LastName) ); return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound(); }) .WithName("UpdateClient") .WithOpenApi(); ``` This code takes into consideration only to update the record not the dependencies of the record. If I pass the `json` with the channels like in the following example ``` { "id": 3, "firstname": "UpdateTest1", "lastname": "UpdateTest2", "channels": [ { "Id": 3, "name": "Other" }, { "Id": 2, "name": "Bing" } ] } ``` Entity Framework Core raises an error because it can’t update the record. Here is the screenshot of the error. ![The error occurs when I try to update - APIs with Entity Framework Core: PUT](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-16.png?resize=640%2C296&ssl=1)The error occurs when I try to update When an object – like in my case the `Client` class – is updated, there are a few things to take into consideration: - do I have to update only the record? - do I have to update the dependencies? - how to add or remove the dependencies from an object? So, the solution is a bit longer than expected. ## Add the Domain mapper First, I have to add a mapping between objects. In this case, the mapping will be between the same object. Why? When the **PUT** method receives the request, we have to be able to match the properties from the parameter with the properties of the record from the database. This is because we want to have the latest representation of the object and then apply the changes. So, the first step is to add a new project for mapping the `Domain` objects with **DTO** (Data Transfer Object). In this case, I don’t have DTOs but only the model from the `Domain`. To do the mapping, I use [AutoMapper](https://automapper.org/). AutoMapper is an object-object mapper. Object-object mapping works by transforming an input object of one type into an output object of a different type. What makes AutoMapper interesting is that it provides some interesting conventions to take the dirty work out of figuring out how to map type A to type B. As long as type B follows AutoMapper’s established convention, almost zero configuration is needed to map two types. So, instead of creating the mapper manually, I use this tool. In this simple example could be easy and quicker to map myself the object. Because I want to use this project as a reference, I add everything I need. ### Add the registration service As I did for the persistence layer, I’m creating a file called `MapperRegistration` that contains all the required configurations for this layer, in particular for AutoMapper. The file is quite simple: ``` public static class MapperRegistration { public static IServiceCollection AddMapperServices(this IServiceCollection services) { services.AddAutoMapper(Assembly.GetExecutingAssembly()); return services; } } ``` Once this file is created, I can call this function in the server project in the Program.cs and use it with like ``` builder.Services.AddMapperServices(); ``` Nice and easy. ### Add the AutoMapper profile Now, the **Profile** explains to AutoMapper what models are a match. In the case of this project, the models are `Client` and `Channel`. Here is the code for this file ``` public class MappingProfile : Profile { public MappingProfile() { CreateMap() .ForMember(x => x.Channels, opt => opt.Ignore()); CreateMap(); } } ``` Although the name of this file can be anything, by convention the name is `MappingProfile`. The definition of the mapping is in the `Create.Map` that explains to AutoMapper what model can be map to what other object. Usually, you have a `Domain` model and a **DTO** (Data Transfer Object). To simplify, in this project, the minimal APIs receive as a parameter an object in the `Domain` structure. ### Add the mapper to the server Atter all of it, I can add the mapper to the server project. So, I open the \_Program.cs\_ and add this line ``` builder.Services.AddMapperServices(); ``` before `builder.Build();`. Now, I am ready to implement the logic in the minimal API. ## Update the PUT verb for Client Now, this is the most complicated part of the project. When the application has to update the `Client` object, I have to bear in mind that the dependencies for `Channel` can be added or removed for the update. Also, I have to remember that only the changes to an object database can be saved in the database again. If I try to save an update for an object that is not coming from the database or I add elements from another object that is not coming from the database, I will face an error of different nature. So, as a general rule, you can update an object in the database with only values from the database. What I have to do now is: - read the full object from the database - identify the updated channels from the PUT parameter - identity the current channels in the database - identify the channels to add - identify the channels to remove - save the object in the database ### Read the full object This is quite straightforward. This is the code ``` var localClient = await db.Clients .Include((c => c.Channels)) .FirstAsync(model => model.Id == id); ``` Here the code is reading from the database the full `Client` object with all the `Channels`. ### Map the new object After that, I have to map the new object from the parameter of the function with the record from the database. Because I need an AutoMapper instance, I have to change the signature of the function like the following code: ``` group.MapPut("/{id}", async Task (long id, Domain.Client client, MyDbContext db, IMapper mapper) => { // ... } ``` So, I added `IMapper mapper` to get the instance. Now, I can map the object like ``` mapper.Map(client, localClient); ``` Now, AutoMapper using the reflection is mapping all the properties of the object from the parameter of the function with the record from the database apart from the `Channels` properties as we set before. ### What to save and what to remove So, the next step is to identify if there is any change in the `Channels` object. I have to list what channels to add to the record. I also have to list what to remove from the record. ``` var updatedChannelIds = client.Channels.Select(c => c.Id).ToList(); var currentChannelIds = localClient.Channels.Select(c => c.Id).ToList(); var channelIdsToAdd = updatedChannelIds.Except(currentChannelIds); var channelIdsToRemove = currentChannelIds.Except(updatedChannelIds); ``` Now that I know the IDs of the channels to add and remove, I can implement this part. ### Remove channels First, I am going to remove the channels from the real record using the channel’s records from the database. I convert the query into a list to avoid future errors. ``` if (channelIdsToRemove.Any()) { var channelsToRemove = localClient.Channels.Where(c => channelIdsToRemove.Contains(c.Id)).ToList(); foreach (var channel in channelsToRemove) localClient.Channels.Remove(channel); } ``` ### Add channels Next step is to add from the database the new list of channels. ``` if (channelIdsToAdd.Any()) { var channelsToAdd = await db.Channels.Where(c => channelIdsToAdd.Contains(c.Id)).ToListAsync(); foreach (var channel in channelsToAdd) localClient.Channels.Add(channel); } ``` ### Save the updated record ``` await db.SaveChangesAsync(); return TypedResults.Ok(); ``` ## Video Finally, if you want to follow me in the creation of this project, watch the following video. ## Wrap up Finally, I have a decent project to use as a future reference based on minimal APIs. The PUT implementation was a bit longer. It was necessary because updating a record with dependencies can be tricky. **Categories:** .NET7, .NET8, .NET9, C# **Tags:** blazor, entity-framework-core, entityframeworkcore, webapi **Hashtags:** blazor, entityframework-core, webapi --- ### [APIs with Entity Framework Core](https://puresourcecode.com/dotnet/csharp/apis-with-entity-framework-core/) **Published:** February 19, 2025 **Author:** Enrico **Excerpt:** I want to give a complete example of minimal APIs in Blazor with Entity Framework Core with complex objects. **Content:** In this new post, I want to give a complete example of minimal APIs in Blazor with Entity Framework Core with complex objects. I always struggle to have a solution working when my model has dependencies with other object. Here I show my test and my code. The code is in [NET9](https://puresourcecode.com/category/dotnet/net9/). In the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/update-related-data?view=aspnetcore-9.0), there are some examples but it is not complex enough. A few days ago, [I posted about another problem](https://puresourcecode.com/dotnet/net9/pendingmodelchangeswarning-with-net9/) I had with NET9 and Entity Framework Core. The full source code of this post is available on GitHub. If you have any questions, please comment below or post in the [forum](https://puresourcecode.com/forum/). The complete code is spanned through those posts: - [APIs with Entity Framework Core](https://puresourcecode.com/dotnet/net9/apis-with-entity-framework-core/) - [APIs with Entity Framework Core: POST](https://puresourcecode.com/dotnet/csharp/apis-with-entity-framework-core-post/) - [APIs with Entity Framework Core: PUT](https://puresourcecode.com/dotnet/net7/apis-with-entity-framework-core-put/) ## Scenario Very often in my projects, I want to save the data in the database. I always use the models I created in the `Domain` project. This is because I like to have a clear code as I explained in my [earlier post](https://puresourcecode.com/dotnet/net-core/architecting-asp-net-core-applications). Let me clear my common problem. ### Joined tables So, let me show a real database I’m working on. As you can see I have a few tables but not all of them are coming strictly from the code. The tables `Clients`, `ClientAddresses` and `tbl_Channels` are created from the `Domain`. `ChannelClient` is a joined table that Entity Framework Core is creating automatically. ![Diagram of my application with objects - Minimal APIs with Entity Framework Core](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-3.png?resize=640%2C919&ssl=1)Diagram of my application with objects To avoid distractions, I only show the code necessary to create the tables with minimal data. So, I can focus only on the implementation of the code to insert or update in the database the data. Once the logic is clear, it is easy to extend it to other fields. ### Tables explanation Now, the implementation has 4 tables. As I said, 3 are generated from the code. One is generated from Entity Framework Core as a joined table. Why? Here is an example. In your form, you have a dropdown list with multiple choices. By the way, in this example, I use my [AutoComplete component](https://puresourcecode.com/dotnet/net-core/autocomplete-component-for-blazor/) for Blazor. ![Example of a dropdown multichoice - Minimal APIs with Entity Framework Core](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-4.png?resize=640%2C166&ssl=1)Example of a dropdown multichoice The list is from the `tbl_Channel`s table. Because the user can choose more than one option, the database has to save all of them. That means, for a client I have to save one or more records. This is a `N:M` relation. There are several `Clients` per `Channel` and the same Client has several Channels. For example: Channel 1 has Channels 1,2,5, Client 1 has Channels 1,2 ![Result of N:M records](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-5.png?resize=265%2C356&ssl=1)Result of N:M records ## Domain implementation As I mentioned before, the model to create the database lies in the `Domain` layer. So, I created a new project for it. ### Channel This table contains the items to display in the dropdown list. The user can select one or more of them. The implementation is quite simple. ``` [Table("tbl_Channels")] public class Channel { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [JsonPropertyName("Id")] public long Id { get; set; } [Required] [JsonPropertyName("name")] public string? Name { get; set; } [JsonIgnore] public ICollection? Client { get; set; } } ``` `Table` attribute defines the name of the table in the database. `Key` attributes defined this field as a key and it is generated automatically by the database. `JsonPropertyName` defines the name of the fields when the application reads or returns this object via API. `JsonIgnore` attribute means that this field won’t be added in the `json`. I added this attribute to avoid recursion when the `json` will be created. Now, I want to link this table with the `Client` table. For that, I added an `ICollection` of `Client` that is the model I’m going to create now. #### Clients This table contains the list of clients. Also, here I have to define the link with the Channel table. Now, I continue with the explanation of how to use APIs with Entity Framework Core and later I will add the `ClientAddress` table. ## Persistence and migrations Now, because I want to create this project as a future reference, I am going to create a **Persistence** layer. Generally speaking, this is where I will create the implementation for the repositories and the context. Also, in this project, I will add the migration using Entity Framework Core. To give you more info, I recorded a video how I create the Persistence project. Also, you see how I generate the migration using Entity Framework Code. In this project, I will add the context for the database and the service registration for this layer (and the context). The project is called `APIEFCore.Persistence`. ### Create MyDbContext First, I have to add the creation of the database context. For that, I created a file called `MyDbContext` and the content is the following ``` public class MyDbContext : DbContext { public MyDbContext(DbContextOptions option) : base(option) { } public DbSet Clients { get; set; } public DbSet Channels { get; set; } } ``` Whit this code, I tell the Entity Framework Core that the context contains 2 tables. Those tables can be used with the names `Clients` and `Channels`. ### Registration service Now, because I want to use this project as a reference, I also created a persistence service registration to register the context and other services in the `IServiceCollection`. The code is the following ``` public static class PersistenceServiceRegistration { public const string ConnectionName = "DefaultConnection"; public static IServiceCollection AddPersistenceServices(this IServiceCollection services, IConfiguration configuration, string cnnStringName = ConnectionName, string? cnnString = null) { if (cnnString == null) cnnString = configuration.GetConnectionString(cnnStringName); services.AddDbContext(options => options.UseSqlServer(cnnString)); return services; } } ``` In order to register this code, I have to add in the server project in the `builder` this service using this line: ``` builder.Services.AddPersistenceServices(builder.Configuration); ``` After that, I can use the database and the context in the application. ### Create migration Now, I have to generate the code to create the database. For that, I use what Entity Framework offers. I open the **Package Manager Console** and here I start to add the migration for the context. Because the solution I created is based on [Blazor](https://puresourcecode.com/category/dotnet/blazor/) with [Individual Authentication](https://puresourcecode.com/?s=individual%20authentication), in the project there are 2 contexts. I created `MyDbContex`t but for the project I have the `ApplicationDbContext`. So, I have to specified what context I want to use. In the `app.settings.json` I can see the connection string for the database that is for a **MSSQLLocalDb**. So, to generate the initial migration for my context, in the Package Manager Console, I have to run the following command ``` add-migration InitialMigration -Context MyDbContext ``` With that, Entity Framework generates the migration for this context. ### Update database Now that the migration is created, I can update the structure of the database. As I said, I have 2 contexts and for that I have to run twice the update. Starting with the `ApplicationDbContext`, in the Package Manager Console, I am going to run the following command ``` update-database -Context ApplicationDbContext ``` This is creating the tables for the [ASPNET Users and Roles tables](https://puresourcecode.com/dotnet/blazor/custom-user-management-with-net8-and-blazor) that I discussed a lot in my earlier posts. Remember to specify the context. Now, I can run a similar command for my context ``` update-database -Context MyDbContext ``` The result of all of it is the creation of the tables as shown in the following screenshot. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-6.png?resize=424%2C480&ssl=1) In the video, I show the structure of the tables that is what you see at the top of this post. ## Minimal APIs creation The next step is to create the minimal APIs based on the models I created in the `Domain` layer. Also, for this step I created a video. To generate the APIs, I am going to use Visual Studio and in particular the Scaffolded Item. ![New Scaffolded Item in Visual Studio](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-8.png?resize=640%2C942&ssl=1)New Scaffolded Item in Visual Studio Now, with this tool, I can generate an API based on my model and Entity Framework. So, select the option **API with read/write endpoints, using Entity Framework**. ![Add New Scaffolded Item](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-9.png?resize=640%2C442&ssl=1)Add New Scaffolded Item Then, I have to choose the following fields: - **Model class**: one of the models in the `Domain` layer - **Endpoints class**: I like to create a new file for each model - **DbContext**: that in this case is MyDbContext After that, I want to use **OpenAPI** and **TypedResults** as checked by default. ![API with read/write endpoints, using Entity Framework](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-10.png?resize=640%2C341&ssl=1)API with read/write endpoints, using Entity Framework After the first endpoint for `Client` is created, I want to add another one for `Channel`. I can’t execute the procedure again because it returns an error. For that, go to the `Program.cs` in the server project and commend ``` app.MapClientEndpoints(); ``` After that, I can generate a new API using the **New Scaffolded Item** option. I’m not sure if it is possible to avoid this error in some way. If you comment this line, it is working. ### Swagger Now, Swagger is added automatically to the project. So, I can play with it. If you use my project, the link is ``` https://localhost:7191/swagger/index.html ``` If I try to create a new `Channel`, it is working and I can see that APIs with Entity Framework Core are there. But this is only the beginning. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-11.png?resize=640%2C639&ssl=1) ## Wrap up In conclusion, in this first post of APIs with Entity Framework Core, I show you how to set the basis of the project and how: - to create the APIs using the Scaffolded tool - the create the database. The difficult part is coming! I have to start to change the code to add the channels to the client. Also, I want to update this record. How can I do it? Soon the next post. Happy coding! **Categories:** .NET9, C# **Tags:** blazor, entity-framework-core, minimal-apis **Hashtags:** entityframework-core, minimal-apis --- ### [APIs with Entity Framework Core: POST](https://puresourcecode.com/dotnet/csharp/apis-with-entity-framework-core-post/) **Published:** February 19, 2025 **Author:** Enrico **Excerpt:** Here how to change the minimal APIs with Entity Framework Core for POST and GET in order to save and retrieve an object with dependencies. **Content:** Continuing the topic, I want to give a complete example of minimal APIs in Blazor with Entity Framework Core with complex objects.I always struggle to have a solution working when my model has dependencies with other object. Here I show my test and my code. The code is in [NET9](https://puresourcecode.com/category/dotnet/net9/). In the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/update-related-data?view=aspnetcore-9.0), there are some examples, but it is not complex enough. A few days ago, [I posted about another problem](https://puresourcecode.com/dotnet/net9/pendingmodelchangeswarning-with-net9/) I had with NET9 and Entity Framework Core. The full source code of this post is available on GitHub. If you have any questions, please comment below or post in the [forum](https://puresourcecode.com/forum/). The complete code is spanned through those posts: - [APIs with Entity Framework Core](https://puresourcecode.com/dotnet/net9/apis-with-entity-framework-core/) - [APIs with Entity Framework Core: POST](https://puresourcecode.com/dotnet/csharp/apis-with-entity-framework-core-post/) - [APIs with Entity Framework Core: PUT](https://puresourcecode.com/dotnet/net7/apis-with-entity-framework-core-put/) ## Send the first POST request In my [previous post](https://puresourcecode.com/dotnet/net9/apis-with-entity-framework-core/), I prepared the minimal APIs using the **Scaffolded Item** tool in [Visual Studio](https://puresourcecode.com/?s=visual%20studio). Also, [Swagger](https://puresourcecode.com/?s=swagger) was added to the project. Now, I want to submit a json to create a new `Client` record. Then, open Swagger and under the `Clients` section. Now, I have to use the **POST** verb to send a valid `json` with all the details. This is a valid `json` to use in order to create a new record. ``` { "firstname": "Test1", "lastname": "Test2", "channels": [ { "Id": 1, "name": "Google" }, { "Id": 2, "name": "Bing" } ] } ``` As a reminder, I have created the 2 records for the `Channels`. Now, when I submit this request, the code raises an error because the 2 entities for the channel already exist in the database. ### The initial code So, in order to avoid this error, the strategy is to ready the channels from the database and add them to the object. The code generated by Visual Studio was this one: ``` group.MapPost("/", async(Domain.Client client, MyDbContext db) => { db.Clients.Add(client); await db.SaveChangesAsync(); return TypedResults.Created($"/api/Client/{client.Id}", client); }) .WithName("CreateClient") .WithOpenApi(); ``` That means the **POST** verb receives in the variable `client` the json object. Then, it is translated to the `Client` model. When I add this object to the database using `db.Clients.Add(client);` the record for the channels that already exist. So, Entity Framework raises an error because I can’t add to the database those values. After saving in the database successfully, the API returns an **HTTP Created 201**. ## The fix As I said before, what I have to do is to replace the channels in the `Client` model with the data from the database. Here is the code. ``` group.MapPost("/", async (Domain.Client client, MyDbContext db) => { if (client.Channels != null && client.Channels.Count > 0) { var list = client.Channels; client.Channels = new List(); foreach (var c in list) { var channel = await db.Channels.FirstOrDefaultAsync(ch => ch.Id == c.Id); if (channel != null) client.Channels.Add(channel); } } db.Clients.Add(client); await db.SaveChangesAsync(); return TypedResults.Created($"/api/Client/{client.Id}", client); }) .WithName("CreateClient") .WithOpenApi(); ``` What is new here? The lines are retrieving the data from the database and adding the object (if exists) to the `Client` object. Now, I run again the creation of the record using Swagger and it is working. ## The result Therefore, the result is that in the database I see the records in the `Clients` table as in the `json`. ![Clients table content - APIs with Entity Framework Core: POST](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-13.png?resize=640%2C406&ssl=1)Clients table content Also, I see that in the `ChannelClient` table, there are records for a `Client`Id with the related `ChannelsId`. ![ChannelClient table content - APIs with Entity Framework Core: POST](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-14.png?resize=640%2C466&ssl=1)ChannelClient table content ## POST APIs with Entity Framework Core video Now, to help you and as a future reference, I created a video with this code. Please, click on it and subscribe to my YouTube channel. ## Retrieve the data with GET At this point, I have a nice POST that saves a `Client` object with its dependencies. What if I want to retrieve this `Client` with the dependencies using the APIs? For that, the **GET** implementation is what I need. The initial implementation doesn’t include the `Channel` details. For that, I have to change the function that returns the `Client` object by Id. The code is the following: ``` group.MapGet("/{id}", async Task (long id, MyDbContext db) => { return await db.Clients.AsNoTracking() .Include(c => c.Channels) .FirstOrDefaultAsync(model => model.Id == id) is Domain.Client model ? TypedResults.Ok(model) : TypedResults.NotFound(); }) .WithName("GetClientById") .WithOpenApi(); ``` The only change in this code from the one generated by Visual Studio is that I added the `include` to ask Entity Framework Core to consider the linked `Channels` table. The result is the full `Client` object. ![Using Swagger to retrieve a full Client object](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2025/02/image-15.png?resize=640%2C639&ssl=1)Using Swagger to retrieve a full Client object ## Wrap up In conclusion, in this post I show how to change the minimal APIs with Entity Framework Core and fix the POST. Also, I updated the **GET** method to return the full `Client` object. But this is not the end. Look for the next post. Happy coding! **Categories:** .NET9, C# **Tags:** blazor, entity-framework-core, entityframeworkcore, webapi **Hashtags:** entityframework-core, webapi --- ### [PendingModelChangesWarning with NET9](https://puresourcecode.com/dotnet/net9/pendingmodelchangeswarning-with-net9/) **Published:** February 17, 2025 **Author:** Enrico **Excerpt:** I am creating with NET9 using Entity Framework Core that returns PendingModelChangesWarning. Here how to understand this error and fix it **Content:** I have several applications I am creating with [NET9](https://puresourcecode.com/category/dotnet/net9/) using Entity Framework Core that returns **PendingModelChangesWarning**. I found out how to understand this error better. ## Scenario In one of my [Blazor](https://puresourcecode.com/?post_tag=blazor) projects, I added some APIs that save data in the database using [Entity Framework Core](https://puresourcecode.com/?s=entity%20framework). Because it was in [NET8](https://puresourcecode.com/category/dotnet/net8/), I updated the project to [NET9](https://puresourcecode.com/category/dotnet/net9/). After the update, I ran again the application and I got the following error: > ``` > > An unhandled exception has occurred while executing the request. > System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Migrations.PendingModelChangesWarning': The model for context 'TracksContext' has pending changes. Add a new migration before updating the database. This exception can be suppressed or logged by passing event ID 'RelationalEventId.PendingModelChangesWarning' to the 'ConfigureWarnings' method in 'DbContext.OnConfiguring' or 'AddDbContext'. > at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition`1.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, TParam arg) > at Microsoft.EntityFrameworkCore.Diagnostics.RelationalLoggerExtensions.PendingModelChangesWarning(IDiagnosticsLogger`1 diagnostics, Type contextType) > at Microsoft.EntityFrameworkCore.Migrations.Internal.Migrator.MigrateAsync(Func`4 seed, String targetMigration, Nullable`1 lockTimeout, CancellationToken cancellationToken) > at MartinCostello.AppleFitnessWorkoutMapper.Services.TrackImporter.ImportTracksAsync(CancellationToken cancellationToken) in /_/src/AppleFitnessWorkoutMapper/Services/TrackImporter.cs:line 27 > at Program.<>c.<<
$>b__0_12>d.MoveNext() in /_/src/AppleFitnessWorkoutMapper/Program.cs:line 201 > --- End of stack trace from previous location --- > at Microsoft.AspNetCore.Http.Generated.F2A4F9050321DD26474371351A8857FE053630885E7CB9BFF3B12464F0B87EE32__GeneratedRouteBuilderExtensionsCore.<>c__DisplayClass6_0.<g__RequestHandler|5>d.MoveNext() in /_/artifacts/obj/AppleFitnessWorkoutMapper/release/Microsoft.AspNetCore.Http.RequestDelegateGenerator/Microsoft.AspNetCore.Http.RequestDelegateGenerator.RequestDelegateGenerator/GeneratedRouteBuilderExtensions.g.cs:line 638 > --- End of stack trace from previous location --- > at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context) > ``` In my point of view, this error is quite cryptic and I don’t understand where the problem was. If I tried to generate a migration for my project using ``` add-migration InitialMigration -Context MyContext ``` the error came up but with the previous version of Entity Framework, the migration was created without raising any complaints. I saw some issues on [GitHub](https://github.com/dotnet/efcore/issues/34431) about it. ## Quick check So, at this point, I started to search PendingModelChangesWarning with NET9. Any of the suggestions I found were applied to my code. Here is a quick check: If you’re using random values or dynamic values like `Guid.NewGuid()` or `DateTime.Now` for seeding data, the model changes every time you run the application. As a result, EFCore detects a difference and generates a new migration each time. This is why you’re seeing the `PendingModelChangesWarning`. It’s a warning that indicates the model has changed and you need to generate a new migration. However, in your case, this creates an infinite loop of migrations. The recommendation to resolve this issue, always use **constant values** when seeding data. This ensures the model remains consistent, preventing unnecessary migrations. *I found later that this is working.* For more details, you can see the [Microsoft documentation](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/breaking-changes). ### Issues with Identity Users and roles Another common issue is related to the creation of **Users** and **Roles** using Identity. Some columns in the Roles and Users tables, despite being `nullable`, must be initialized during seeding. For example, `ConcurrencyStamp` and `SecurityStamp`. To be aware of these columns, run the migration again. The new migration will show you these columns in the `UpdateData` format. Only initialize these fields in the seed data. Something like this: ``` var Role1 = new IdentityRole() { Id = "6ac343b0-00ef-4a1c-8f64-68daaca77b5b", Name = "Role1", NormalizedName = "Role1".ToUpper(), ConcurrencyStamp = "6ac343b0-00ef-4a1c-8f64-68daaca77b5b" }; builder.Entity().HasData(Role1s); var adminUser = new IdentityUser() { Id = "08beacc0-38dd-42a9-82c1-c3706a0cf19e", Email = "admin@puresourcecode.com", NormalizedEmail = "admin@puresourcecode.com".ToUpper(), UserName = "admin@puresourcecode.com", NormalizedUserName = "admin@puresourcecode.com".ToUpper(), ConcurrencyStamp = "08beacc0-38dd-42a9-82c1-c3706a0cf19e", SecurityStamp = "08beacc0-38dd-42a9-82c1-c3706a0cf19e", PasswordHash = "$2y$10$bDDaBlqNVSIfhAL7UBFjA2sDVVQABeMd" }; builder.Entity().HasData(adminUser); ``` Although I create users and roles, this is not my case ### Snapshots Entity Framework Core compares two versions of your model to detect the changes. It compares **snapshots** from the last migration and the current state. When your model contains some random generated value which is re-evaluated every time to a new value (like using Guid.NewGuid()) as the Id of some seed entity), the new value is considered as a change to the model. The recommended approach would be to hardcode some specific values into your data seeding code. In your case, you are not initializing the `Id` of seed `IdentityRole` data. But what are the consequences of suppressing the error? The purpose of this exception is only to avoid the situation in which a programmer makes changes to the model but forgets to create a migration. Then they get into updating the database and not see the expected changes. Hence, I think the only downside of suppressing this error is that you may get into that situation. > The main motivation behind this change was something that happens often: you make some change to your model (e.g. add a property), and then you forget to actually add a migration to apply it to the database. You run `MigrateAsync()` (or similar) and can’t figure out why your database hasn’t been updated. The new error clearly tells you that your model contains changes with respect to the last migration snapshot. In other words, the point here is to help you avoid accidental error. ## Solution After a few hours of binging and googling, I discovered that it is possible to get more details about the error adding those lines in the configuration of the context: ``` using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; services.AddDbContext(options => options.UseSqlServer(cnnString) .ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)) .EnableDetailedErrors() ); ``` So, those lines show more details about the error and the exact reason or reasons. In my case, the error was related to the configuration of the relation between 2 tables. So, nothing similar to the most common issues I found on the internet. ## Wrap up In conclusion, I hope this post about PendingModelChangesWarning with NET9 could helps you in find your error in the code. If you have any issue or update or you want to share your solution, please comment below or in the [forum](https://puresourcecode.com/forum/). **Categories:** .NET9 **Tags:** efcore, entityframeworkcore, net9 **Hashtags:** efcore, net9 --- ### [NET9 is here](https://puresourcecode.com/dotnet/net9/net9-is-here/) **Published:** November 14, 2024 **Author:** Enrico **Excerpt:** NET 9 is here and out today. In this post, I highlight the top updates across 8 key areas. Are you ready to migrate to NET9? **Content:** NET 9 is here and out today. Top updates across 8 key areas: ## 𝗖# 𝟭𝟯 Params collections, enhanced lock types, and a new escape make coding smoother. Plus, implicit index access to simplify initialization. ## 𝗔𝗦𝗣.𝗡𝗘𝗧 𝗖𝗼𝗿𝗲 OpenAPI enhancements, faster asset delivery, PAR support, and the new HybridCache library boost performance and security. ## 𝗘𝗙 𝗖𝗼𝗿𝗲 Improved Cosmos DB support, pre-compiled AOT queries, better LINQ, and streamlined migrations make data handling faster. ## 𝗥𝘂𝗻𝘁𝗶𝗺𝗲 Feature switches, control-flow enforcement, and dynamic adaptation improve performance across different app sizes. ## 𝗟𝗶𝗯𝗿𝗮𝗿𝗶𝗲𝘀 Base64Url, new TimeSpan overloads, new collections, and cryptography upgrades add flexibility and security. ## 𝗦𝗗𝗞 Run tests in parallel, new terminal logger by default, a workload sets feature, and more analyzers for better productivity. ## .𝗡𝗘𝗧 𝗔𝘀𝗽𝗶𝗿𝗲 New MSBuild SDK, an improved dashboard, telemetry updates, support to wait for dependencies, and lots more. ## .𝗡𝗘𝗧 𝗠𝗔𝗨𝗜 Opt into Native AOT deployment, embedding APIs, full trimming, and new controls for smaller, faster mobile apps. Are you upgrading to .NET 9? **Categories:** .NET9 **Tags:** net9 **Hashtags:** net9 --- ### [Connect MongoDB with Node.js](https://puresourcecode.com/dotnet/net7/connect-mongodb-with-node-js/) **Published:** October 20, 2024 **Author:** Enrico Rossini **Excerpt:** In this new post, I show how to connect our application to MongoDB using Node.js. Send users and test your application using an APIs client. **Content:** In this new post, I show how to connect our application to MongoDB using Node.js. Let’s start with the installation of MongoDB if you want to test your application locally. The source code of this post is available on [GitHub](https://github.com/erossini/BirkbeckPiazza). ## Install MongoDB First, if we want to play locally on our laptop, we have to install MongoDB. Download the installer from this link and follow the instructions. After you run the UI, you should see a similar screenshot. ![Local MongoDB - Connect MongoDB with Node.js](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-7.png?resize=640%2C376&ssl=1)Local MongoDB ## MongoDB on line There is another option: use MongoDB online. To do so, you have to create a free account on the [MongoDB website](https://account.mongodb.com/account/login?signedOut=true). You should see the following screenshot. ![MongoDB website - Connect MongoDB with Node.js](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-8.png?resize=640%2C376&ssl=1)MongoDB website Now, the first activity we have to do to use MongoDB online is to create an organization. ![MongoDB requires to create an organization](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-9.png?resize=640%2C376&ssl=1)MongoDB requires to create an organization The next screen is asking for a selection of the type of the database we want to use. My choice is to select **MongoDB Atlas**. ![Create an organization in MongoDB](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-10.png?resize=600%2C885&ssl=1)Create an organization in MongoDB Then, click **Next**. On the next screen, we can add more members and other basic settings. At the moment, nothing is required. ![Create organization - Second screen](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-11.png?resize=617%2C464&ssl=1)Create organization – Second screen Now, click **Create Organization**. At the end, you are redirected to the **Project** page. ![Projects page in MongoDB](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-12.png?resize=640%2C426&ssl=1)Projects page in MongoDB ## Create a new project So, clicking on the button **New Project**, we are redirected to the **Create a Project page**. ![Create a new project](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-14.png?resize=640%2C426&ssl=1)Create a new project Here, we can insert the **Name of Your Project**. Also, it is possible to add tags that help you to identify and categorize your project. ![Create new project - Screen 2](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-15.png?resize=640%2C426&ssl=1)Create a new project – Screen 2 Now, on the second screen, we can add more users that can access the database. Click on the button **Create Project**. After this action is complete, we can see the **Project Settings**. ![Project setting page](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-16.png?resize=640%2C426&ssl=1)Project setting page ## Create a cluster The next step to have a usable MongoDB database is to create a **cluster**. A free cluster provides a small-scale development environment to host your data. Free clusters never expire, and provide access to a subset of Atlas features and functionality. ![Create a cluster](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-17.png?resize=640%2C426&ssl=1)Create a cluster So, now we have to click on the **Create** button. In the next screen, we have to choose a few settings for the cluster. The name of the cluster is not changeable. The type of service I choose is **M0** because it is free and it is perfect for my tests. ![Deploy your cluster with MongoDB](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-19.png?resize=640%2C426&ssl=1)Deploy your cluster with MongoDB Then, I have to choose the **Provider** and my choice is [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/). Then, I have to select the **Region** and I choose **Ireland** because it is close to where I live. Then, click on **Create Deployment**. ![Connect to the cluster](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-20.png?resize=640%2C555&ssl=1)Connect to the cluster Now, I can see that the cluster is created and my IP is added to the access list. Plus, an account is been created for me to access the cluster. This form offers you a **Username** and a **Password**. You can change those and then click on the button **Create Database User**. ![Cluster created](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-21.png?resize=640%2C419&ssl=1)Cluster created So that, the cluster is created and I am ready to **Choose a connection method**. Because I want to connect my Node.js project to the MongoDB, I choose the first option **Drivers**. ![Choose the connection type](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-22.png?resize=640%2C656&ssl=1)Choose the connection type Then, I have the instructions on how to install the driver for Node.js and what the connection string is. The next step is to create a simple project in Node.js to connect to the database. ![Connection settings](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-23.png?resize=640%2C782&ssl=1)Connection settings ## Create a project with Node.js In my previous post called [Introduction to Node.js](https://puresourcecode.com/tools/node-js/introduction-to-node-js/), I showed how to start a project with Node.js. So, I have to install a package for my project called **mongoose** that helps with the connection to the MongoDB database. In the Terminal, type and execute this command ``` npm install mongoose ``` Now, in the app.js, I require to use this package in my project adding at the top this line ``` const mongoose = require('mongoose') ``` So, I want to connect my project to the database using the variable `mongoose`. For that, I can use the function `connect` and pass the connection string that I can copy from the screen above. Now, the following code open the connection and write a text in the console ``` mongoose.connect('') .then(() => { console.log('Connected with MongoDB') }) .catch((err) => { console.error('Error connection to MongoDB', err) }) ``` Run the application and check that the connection is up and running. ### Where is the database name? If you use the connection string from the screen above, for example ``` mongodb+srv://erossi03:pw2@cluster0.j09uw.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0 ``` when I run the application, MongoDB creates a new collection called `test`. Probably, this is not what I want. If I want the add the name of the collection, add the name before the `?`, for example ``` mongodb+srv://erossi03:pw2@cluster0.j09uw.mongodb.net/MyDatabase?retryWrites=true&w=majority&appName=Cluster0 ``` ## Create a User model We have to tell MongoDB what kind of data we want to save. Like other languages, I have to define in the code a `model` for the data. Thing a model in NoSQL databases like MongoDB as a schema of the table in a relational database. So, my first model will be to save the base data for a user. So, in the root of the project, create a `model` folder that conventionally has all the schema. Then, create a file called `user.js` where I define the structure. As a convention, the name of the schema is the name of the content using the singular word (for example user not users). The collection will be plural, for example `users`. This a collection as a table in a relational database. ``` const mongoose = require('mongoose') const userSchema = new mongoose.Schema({ username: { type: String, required: true, min: 3, max: 256 }, email: { type: String, required: true, min: 3, max: 256 }, password: { type: String, required: true, min: 6, max: 256 }, createdAt: { type: Date, default: Date.now } }) module.exports = mongoose.model('User', userSchema); ``` In this code, `mongoose` is required because is the package that helps with the connection with the database but also with the definition of the schema and work on the collections. To define the schema, I use the function `Schema` that needs as a parameter the structure of the collection. Each “field” is define straight in the structure and then in the `{}` there is the definition of the table. The types we can use are define from the `SchemaTypes` available with examples on the [official documentation](https://mongoosejs.com/docs/schematypes.html) and they are - [String](https://mongoosejs.com/docs/schematypes.html#strings) - [Number](https://mongoosejs.com/docs/schematypes.html#numbers) - [Date](https://mongoosejs.com/docs/schematypes.html#dates) - [Buffer](https://mongoosejs.com/docs/schematypes.html#buffers) - [Boolean](https://mongoosejs.com/docs/schematypes.html#booleans) - [Mixed](https://mongoosejs.com/docs/schematypes.html#mixed) - [ObjectId](https://mongoosejs.com/docs/schematypes.html#objectids) - [Array](https://mongoosejs.com/docs/schematypes.html#arrays) - [Decimal128](https://mongoosejs.com/docs/api/mongoose.html#mongoose_Mongoose-Decimal128) - [Map](https://mongoosejs.com/docs/schematypes.html#maps) - [Schema](https://mongoosejs.com/docs/schematypes.html#schemas) - [UUID](https://mongoosejs.com/docs/schematypes.html#uuid) - [BigInt](https://mongoosejs.com/docs/schematypes.html#bigint) For example, the “field” `username` is a required `String` with minimum 3 characters and maximum 156. After defining all the fields, we have to export the schema using ``` module.exports = mongoose.model('User', userSchema); ``` ## Add User route Now, I have to create a route for `Users` using the model I created above. I create a new folder called `routes` where I will add all the routes from my application. Now, I create a new file `users.j`s. In this file I’m going to define a new router, add the model and export the router. ``` const express = require('express') const router = express.Router(); const User = require('../models/user') module.exports = router; ``` This code is enough to create the collection in MongoDB. If you look at your cluster or local instance, you can see your new database ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-28.png?resize=640%2C445&ssl=1)The database is created in MongoDB ### Add the register path As you can see in the code above, I added a new `post` function to `register` a new user. That means I can consume the path `/api/users/register` to register a new user using a `POST` HTTP verbs. Also, I have to pass as a payload email, username and password as a json. An example of that is here ``` { "email": "erossi03@student.birkbeck.ac.uk", "username": "test", "password": "Test!2024" } ``` In order to read the body of the request, I have to use another package called `body-parser`. I have to add this package only in the main file of the application, usually `app.js`. This package gives us the ability to read the body of the request and parse the request. See the following code ``` const express = require('express') const router = express.Router(); const User = require('../models/user') router.post('/register', async (req, res) => { // prepare the data to save in the database const user = new User({ username: req.body.username, email: req.body.email, password: req.body.password }); // save the user in the database and return the saved user const savedUser = await user.save(); res.send(savedUser); }) module.exports = router; ``` `req.body.username` is what the package is doing. Basically, the `req` contains in the `body` the json passed in the `POST` request. Now, I can tell to search the tag `username` in this json using the package. If I fast forward to the result, you can see in the following screenshot how I call the function and get the result. I’m using [Thunder Client](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client) or [Postman](https://www.postman.com/downloads/) to send the requests for test because it is integrated in Visual Studio Code. ![Using Thunder Client to test the application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-25.png?resize=640%2C386&ssl=1)Using Thunder Client to test the application Then, to save the user in the collection, I use the function `save` and then return using the response `res` the full json of the new user. ## Add the route on the app The past part is to add the route and other packages (like `body-parser`) to our application. So, I replace the `app.js` content with the following ``` // add packages dependencies const express = require('express') const mongoose = require('mongoose') // Importing the body-parser to parse the json const bodyParser = require('body-parser'); // importing routes (middleware) const userRoute = require('./routes/users') // define the application const app = express() // add body-parser to the app app.use(bodyParser.json()); // add routes app.use('/api/users', userRoute); // open the connection with the database mongoose.connect('mongodb+srv://erossi03:pw2@cluster0.j09uw.mongodb.net/PiazzaDatabase?retryWrites=true&w=majority&appName=Cluster0') .then(() => { console.log('Connected with MongoDB') }) .catch((err) => { console.error('Error connection to MongoDB', err) }); app.listen(3000, () => { console.log('The application Piazza is up and running') }); ``` Now, it is time to run and application and play with it. Try to send a request using one of suggested tool and doing your tests. ## Wrap up I hope this post can help you to start working with MongoDB in your Node.js project. Let me know if you need more clarification. **Categories:** .NET7 --- ### [Variables and simple data types in Python](https://puresourcecode.com/programming-languages/python/variables-and-simple-data-types-in-python/) **Published:** October 14, 2024 **Author:** Enrico Rossini **Excerpt:** Continue the series of posts about Python: this new post is about variables and simple data types and constants. **Content:** Continue the series of posts about Python: this new post is about variables and simple data types and constants. - [Hello Python](https://puresourcecode.com/programming-languages/python/hello-python/) - [Variables and simple data types in Python](https://puresourcecode.com/programming-languages/python/variables-and-simple-data-types-in-python/) - [Getting started with Python](https://www.puresourcecode.com/programming-languages/python/getting-started-with-python/) - [](https://www.puresourcecode.com/programming-languages/python/bitmap-message-in-python/)[Bitmap message in Python](https://www.puresourcecode.com/programming-languages/python/bitmap-message-in-python/) - [](https://www.puresourcecode.com/programming-languages/python/play-blackjack-with-python/)[Play blackjack with Python](https://www.puresourcecode.com/programming-languages/python/play-blackjack-with-python/) - [](https://www.puresourcecode.com/programming-languages/python/bagels-a-logic-game-in-python/)[Bagels a logic game in Python](https://www.puresourcecode.com/programming-languages/python/bagels-a-logic-game-in-python/) ## Starting Let’s take a closer look at what Python does when you run hello\_world.py. As it turns out, Python does a fair amount of work, even when it runs a simple program: hello\_world.py ``` print("Hello Python world!") ``` When you run this code, you should see the following output: ``` Hello Python world! ``` When you run the file hello\_world.py, the ending .py indicates that the file is a Python program. Your editor then runs the file through the Python interpreter, which reads through the program and determines what each word in the program means. For example, when the interpreter sees the word print followed by parentheses, it prints to the screen whatever is inside the parentheses. As you write your programs, your editor highlights different parts of your program in different ways. For example, it recognizes that print() is the name of a function and displays that word in one color. It recognizes that “Hello Python world!” is not Python code, and displays that phrase in a different color. This feature is called syntax highlighting and is quite useful as you start to write your own programs. ## Variables Let’s try using a variable in hello\_world.py. Add a new line at the beginning of the file, and modify the second line: **hello\_world.py** ``` message = "Hello Python world!" print(message) ``` Run this program to see what happens. You should see the same output you saw previously: ``` Hello Python world! ``` We’ve added a variable named message. Every variable is connected to a value, which is the information associated with that variable. In this case the value is the “Hello Python world!” text. Adding a variable makes a little more work for the Python interpreter. When it processes the first line, it associates the variable message with the “Hello Python world!” text. When it reaches the second line, it prints the value associated with message to the screen. Let’s expand on this program by modifying hello\_world.py to print a second message. Add a blank line to hello\_world.py, and then add two new lines of code: ``` message = "Hello Python world!" print(message) message = "Hello Python Crash Course world!" print(message) ``` Now when you run *hello\_world.py*, you should see two lines of output: ``` Hello Python world! Hello Python Crash Course world! ``` You can change the value of a variable in your program at any time, and Python will always keep track of its current value. ## Naming and Using Variables When you’re using variables in Python, you need to adhere to a few rules and guidelines. Breaking some of these rules will cause errors; other guidelines just help you write code that’s easier to read and understand. Be sure to keep the following rules in mind when working with variables: - Variable names can contain only letters, numbers, and underscores. They can start with a letter or an underscore, but not with a number. For instance, you can call a variable `message_1` but not `1_message`. - Spaces are not allowed in variable names, but underscores can be used to separate words in variable names. For example, `greeting_message` works but `greeting message` will cause errors. - Avoid using Python keywords and function names as variable names. For example, do not use the word `print` as a variable name; Python has reserved it for a particular programmatic purpose. - Variable names should be short but descriptive. For example, `name` is better than `n`, `student_name` is better than `s_n`, and `name_length` is better than `length_of_persons_name`. - Be careful when using the lowercase letter l and the uppercase letter O because they could be confused with the numbers 1 and 0. - It can take some practice to learn how to create good variable names, especially as your programs become more interesting and complicated. As you write more programs and start to read through other people’s code, you’ll get better at coming up with meaningful names. ## Avoiding Name Errors When Using Variables Every programmer makes mistakes, and most make mistakes every day. Although good programmers might create errors, they also know how to respond to those errors efficiently. Let’s look at an error you’re likely to make early on and learn how to fix it. We’ll write some code that generates an error on purpose. Enter the following code, including the misspelled word `mesage`: ``` message = "Hello Python Crash Course reader!" print(message) ``` When an error occurs in your program, the Python interpreter does its best to help you figure out where the problem is. The interpreter provides a traceback when a program cannot run successfully. A traceback is a record of where the interpreter ran into trouble when trying to execute your code. Here’s an example of the traceback that Python provides after you’ve accidentally misspelled a variable’s name: ``` Traceback (most recent call last): ❶ File "hello_world.py", line 2, in ❷ print(mesage) ^^^^^^ ❸ NameError: name 'mesage' is not defined. Did you mean: 'message'? ``` The output reports that an error occurs in line 2 of the file *hello\_world.py* ❶. The interpreter shows this line ❷ to help us spot the error quickly and tells us what kind of error it found ❸. In this case it found a name error and reports that the variable being printed, `mesage`, has not been defined. Python can’t identify the variable name provided. A name error usually means we either forgot to set a variable’s value before using it, or we made a spelling mistake when entering the variable’s name. If Python finds a variable name that’s similar to the one it doesn’t recognize, it will ask if that’s the name you meant to use. In this example we omitted the letter s in the variable name `message` in the second line. The Python interpreter doesn’t spellcheck your code, but it does ensure that variable names are spelled consistently. For example, watch what happens when we spell message incorrectly in the line that defines the variable: ``` mesage = "Hello Python Crash Course reader!" print(mesage) ``` In this case, the program runs successfully! ``` Hello Python Crash Course reader! ``` The variable names match, so Python sees no issue. Programming languages are strict, but they disregard good and bad spelling. As a result, you don’t need to consider English spelling and grammar rules when you’re trying to create variable names and writing code. Many programming errors are simple, single-character typos in one line of a program. If you find yourself spending a long time searching for one of these errors, know that you’re in good company. Many experienced and talented programmers spend hours hunting down these kinds of tiny errors. Try to laugh about it and move on, knowing it will happen frequently throughout your programming life. ## Variables Are Labels Variables are often described as boxes you can store values in. This idea can be helpful the first few times you use a variable, but it isn’t an accurate way to describe how variables are represented internally in Python. It’s much better to think of variables as labels that you can assign to values. You can also say that a variable references a certain value. This distinction probably won’t matter much in your initial programs, but it’s worth learning earlier rather than later. At some point, you’ll see unexpected behavior from a variable, and an accurate understanding of how variables work will help you identify what’s happening in your code. ## Strings Because most programs define and gather some sort of data and then do something useful with it, it helps to classify different types of data. The first data type we’ll look at is the string. Strings are quite simple at first glance, but you can use them in many different ways. A string is a series of characters. Anything inside quotes is considered a string in Python, and you can use single or double quotes around your strings like this: ``` "This is a string." 'This is also a string.' ``` This flexibility allows you to use quotes and apostrophes within your strings: ``` 'I told my friend, "Python is my favorite language!"' "The language 'Python' is named after Monty Python, not the snake." "One of Python's strengths is its diverse and supportive community." ``` Let’s explore some of the ways you can use strings. ### Changing Case in a String with Methods One of the simplest tasks you can do with strings is change the case of the words in a string. Look at the following code, and try to determine what’s happening: *name.py* ``` name = "ada lovelace" print(name.title()) ``` Save this file as name.py and then run it. You should see this output: ``` Ada Lovelace ``` In this example, the variable `name` refers to the lowercase string `"ada lovelace"`. The method `title()` appears after the variable in the `print()` call. A *method* is an action that Python can perform on a piece of data. The dot (.) after name in `name.title()` tells Python to make the `title()` method act on the variable name. Every method is followed by a set of parentheses, because methods often need additional information to do their work. That information is provided inside the parentheses. The title() function doesn’t need any additional information, so its parentheses are empty. The `title()` method changes each word to title case, where each word begins with a capital letter. This is useful because you’ll often want to think of a name as a piece of information. For example, you might want your program to recognize the input values `Ada`, `ADA`, and `ada` as the same name, and display all of them as Ada. Several other useful methods are available for dealing with case as well. For example, you can change a string to all uppercase or all lowercase letters like this: ``` name = "Ada Lovelace" print(name.upper()) print(name.lower()) ``` This will display the following: ``` ADA LOVELACE ada lovelace ``` The `lower()` method is particularly useful for storing data. You typically won’t want to trust the capitalization that your users provide, so you’ll convert strings to lowercase before storing them. Then when you want to display the information, you’ll use the case that makes the most sense for each string. ## Using Variables in Strings In some situations, you’ll want to use a variable’s value inside a string. For example, you might want to use two variables to represent a first name and a last name, respectively, and then combine those values to display someone’s full name: **full\_name.py** ``` first_name = "ada" last_name = "lovelace" ❶ full_name = f"{first_name} {last_name}" print(full_name) ``` To insert a variable’s value into a string, place the letter f immediately before the opening quotation mark ❶. Put braces around the name or names of any variable you want to use inside the string. Python will replace each variable with its value when the string is displayed. These strings are called f-strings. The `f` is for format, because Python formats the string by replacing the name of any variable in braces with its value. The output from the previous code is: ``` ada lovelace ``` You can do a lot with `f-strings`. For example, you can use f-strings to compose complete messages using the information associated with a variable, as shown here: ``` first_name = "ada" last_name = "lovelace" full_name = f"{first_name} {last_name}" ❶ print(f"Hello, {full_name.title()}!") ``` The full name is used in a sentence that greets the user ❶, and the title() method changes the name to title case. This code returns a simple but nicely formatted greeting: ``` Hello, Ada Lovelace! ``` You can also use f-strings to compose a message, and then assign the entire message to a variable: ``` first_name = "ada" last_name = "lovelace" full_name = f"{first_name} {last_name}" ❶ message = f"Hello, {full_name.title()}!" ❷ print(message) ``` This code displays the message Hello, Ada Lovelace! as well, but by assigning the message to a variable ❶ we make the final print() call much simpler ❷. ### Adding Whitespace to Strings with Tabs or Newlines In programming, whitespace refers to any nonprinting characters, such as spaces, tabs, and end-of-line symbols. You can use whitespace to organize your output so it’s easier for users to read. To add a tab to your text, use the character combination \\t: ``` >>> print("Python") Python >>> print("\tPython") Python ``` To add a newline in a string, use the character combination `\n`: ``` >>> print("Languages:\nPython\nC\nJavaScript") Languages: Python C JavaScript ``` > > > Newlines and tabs will be very useful in the next two chapters, when you start to produce many lines of output from just a few lines of code. ## Stripping Whitespace Extra whitespace can be confusing in your programs. To programmers, `'python'` and `'python '` look pretty much the same. But to a program, they are two different strings. Python detects the extra space in `'python '` and considers it significant unless you tell it otherwise. It’s important to think about whitespace, because often you’ll want to compare two strings to determine whether they are the same. For example, one important instance might involve checking people’s usernames when they log in to a website. Extra whitespace can be confusing in much simpler situations as well. Fortunately, Python makes it easy to eliminate extra whitespace from data that people enter. Python can look for extra whitespace on the right and left sides of a string. To ensure that no whitespace exists at the right side of a string, use the `rstrip()` method: ``` ❶ >>> favorite_language = 'python ' ❷ >>> favorite_language 'python ' ❸ >>> favorite_language.rstrip() 'python' ❹ >>> favorite_language 'python ' ``` The value associated with `favorite_language` ❶ contains extra whitespace at the end of the string. When you ask Python for this value in a terminal session, you can see the space at the end of the value ❷. When the `rstrip()` method acts on the variable `favorite_language` ❸, this extra space is removed. However, it is only removed temporarily. If you ask for the value of favorite\_language again, the string looks the same as when it was entered, including the extra whitespace ❹. To remove the whitespace from the string permanently, you have to associate the stripped value with the variable name: ``` favorite_language = 'python ' ❶ >>> favorite_language = favorite_language.rstrip() favorite_language 'python' ``` > > > To remove the whitespace from the string, you strip the whitespace from the right side of the string and then associate this new value with the original variable ❶. Changing a variable’s value is done often in programming. This is how a variable’s value can be updated as a program is executed or in response to user input. You can also strip whitespace from the left side of a string using the `lstrip()` method, or from both sides at once using strip(): ``` ❶ >>> favorite_language = ' python ' ❷ >>> favorite_language.rstrip() ' python' ❸ >>> favorite_language.lstrip() 'python ' ❹ >>> favorite_language.strip() 'python' ``` In this example, we start with a value that has whitespace at the beginning and the end ❶. We then remove the extra space from the right side ❷, from the left side ❸, and from both sides ❹. Experimenting with these stripping functions can help you become familiar with manipulating strings. In the real world, these stripping functions are used most often to clean up user input before it’s stored in a program. ## Removing Prefixes When working with strings, another common task is to remove a prefix. Consider a URL with the common prefix https://. We want to remove this prefix, so we can focus on just the part of the URL that users need to enter into an address bar. Here’s how to do that: ``` nostarch_url = 'https://nostarch.com' nostarch_url.removeprefix('https://') 'nostarch.com' ``` > > Enter the name of the variable followed by a dot, and then the method `removeprefix()`. Inside the parentheses, enter the prefix you want to remove from the original string. Like the methods for removing whitespace, removeprefix() leaves the original string unchanged. If you want to keep the new value with the prefix removed, either reassign it to the original variable or assign it to a new variable: ``` simple_url = nostarch_url.removeprefix('https://') ``` > > > When you see a URL in an address bar and the `https://` part isn’t shown, the browser is probably using a method like removeprefix() behind the scenes. ## Avoiding Syntax Errors with Strings One kind of error that you might see with some regularity is a syntax error. A syntax error occurs when Python doesn’t recognize a section of your program as valid Python code. For example, if you use an apostrophe within single quotes, you’ll produce an error. This happens because Python interprets everything between the first single quote and the apostrophe as a string. It then tries to interpret the rest of the text as Python code, which causes errors. Here’s how to use single and double quotes correctly. Save this program as apostrophe.py and then run it: **apostrophe.py** ``` message = "One of Python's strengths is its diverse community." print(message) ``` The apostrophe appears inside a set of double quotes, so the Python interpreter has no trouble reading the string correctly: ``` One of Python's strengths is its diverse community. ``` However, if you use single quotes, Python can’t identify where the string should end: ``` message = 'One of Python's strengths is its diverse community.' print(message) ``` You’ll see the following output: ``` File "apostrophe.py", line 1 message = 'One of Python's strengths is its diverse community.' ❶ ^ SyntaxError: unterminated string literal (detected at line 1) ``` In the output you can see that the error occurs right after the final single quote ❶. This syntax error indicates that the interpreter doesn’t recognize something in the code as valid Python code, and it thinks the problem might be a string that’s not quoted correctly. Errors can come from a variety of sources, and I’ll point out some common ones as they arise. You might see syntax errors often as you learn to write proper Python code. Syntax errors are also the least specific kind of error, so they can be difficult and frustrating to identify and correct. ## Numbers Numbers are used quite often in programming to keep score in games, represent data in visualizations, store information in web applications, and so on. Python treats numbers in several different ways, depending on how they’re being used. Let’s first look at how Python manages integers, because they’re the simplest to work with. ### Integers You can add (`+`), subtract (`-`), multiply (`*`), and divide (`/`) integers in Python. ``` >>> 2 + 3 5 >>> 3 - 2 1 >>> 2 * 3 6 >>> 3 / 2 1.5 ``` > > > In a terminal session, Python simply returns the result of the operation. Python uses two multiplication symbols to represent exponents: ``` >>> 3 ** 2 9 >>> 3 ** 3 27 >>> 10 ** 6 1000000 ``` Python supports the order of operations too, so you can use multiple operations in one expression. You can also use parentheses to modify the order of operations so Python can evaluate your expression in the order you specify. For example: ``` >>> 2 + 3*4 14 >>> (2 + 3) * 4 20 ``` The spacing in these examples has no effect on how Python evaluates the expressions; it simply helps you more quickly spot the operations that have priority when you’re reading through the code. ### Floats Python calls any number with a decimal point a float. This term is used in most programming languages, and it refers to the fact that a decimal point can appear at any position in a number. Every programming language must be carefully designed to properly manage decimal numbers so numbers behave appropriately, no matter where the decimal point appears. For the most part, you can use floats without worrying about how they behave. Simply enter the numbers you want to use, and Python will most likely do what you expect: ``` >>> 0.1 + 0.1 0.2 >>> 0.2 + 0.2 0.4 >>> 2 * 0.1 0.2 >>> 2 * 0.2 0.4 ``` > > However, be aware that you can sometimes get an arbitrary number of decimal places in your answer ``` >>> 0.2 + 0.1 0.30000000000000004 >>> 3 * 0.1 0.30000000000000004 ``` This happens in all languages and is of little concern. Python tries to find a way to represent the result as precisely as possible, which is sometimes difficult given how computers have to represent numbers internally. Just ignore the extra decimal places for now. ### Integers and Floats When you divide any two numbers, even if they are integers that result in a whole number, you’ll always get a float: ``` >>> 4/2 2.0 ``` > > If you mix an integer and a float in any other operation, you’ll get a float as well ``` >>> 1 + 2.0 3.0 >>> 2 * 3.0 6.0 >>> 3.0 ** 2 9.0 ``` Python defaults to a float in any operation that uses a float, even if the output is a whole number. ### Underscores in Numbers When you’re writing long numbers, you can group digits using underscores to make large numbers more readable: ``` >>> universe_age = 14_000_000_000 ``` > > When you print a number that was defined using underscores, Python prints only the digits: ``` >>> print(universe_age) 14000000000 ``` Python ignores the underscores when storing these kinds of values. Even if you don’t group the digits in threes, the value will still be unaffected. To Python, 1000 is the same as 1\_000, which is the same as 10\_00. This feature works for both integers and floats. ### Multiple Assignment You can assign values to more than one variable using just a single line of code. This can help shorten your programs and make them easier to read; you’ll use this technique most often when initializing a set of numbers. For example, here’s how you can initialize the variables x, y, and z to zero: ``` >>> x, y, z = 0, 0, 0 ``` > > > You need to separate the variable names with commas, and do the same with the values, and Python will assign each value to its respective variable. As long as the number of values matches the number of variables, Python will match them up correctly. ## Constants A constant is a variable whose value stays the same throughout the life of a program. Python doesn’t have built-in constant types, but Python programmers use all capital letters to indicate a variable should be treated as a constant and never be changed: ``` MAX_CONNECTIONS = 5000 ``` When you want to treat a variable as a constant in your code, write the name of the variable in all capital letters. **Categories:** Python **Tags:** python **Hashtags:** python --- ### [Hello Python](https://puresourcecode.com/programming-languages/python/hello-python/) **Published:** October 12, 2024 **Author:** Enrico Rossini **Excerpt:** Hello Python! is a new thread in my blog. I started to learn this programming language a few months ago but now I have to be serious. **Content:** Hello [Python](https://puresourcecode.com/programming-languages/python/getting-started-with-python/)! is a new thread in my blog. I started to learn this programming language a few months ago but now I have to use it for university. So, I have to practice. Python differs slightly on different operating systems, so you’ll need to keep a few considerations in mind. In the following sections, we’ll make sure Python is set up correctly on your system. ## Python Versions Every programming language evolves as new ideas and technologies emerge, and the developers of Python have continually made the language more versatile and powerful. As of this writing, the latest version is Python 3.11, but everything in this book should run on Python 3.9 or later. In this section, we’ll find out if Python is already installed on your system and whether you need to install a newer version. Appendix A contains additional details about installing the latest version of Python on each major operating system as well. ## Running Snippets of Python Code You can run Python’s interpreter in a terminal window, allowing you to try bits of Python code without having to save and run an entire program. Throughout this book, you’ll see code snippets that look like this: ``` >>> print("Hello Python interpreter!") Hello Python interpreter! ``` The three angle brackets (`>>>`) prompt, which we’ll refer to as a *Python prompt*, indicates that you should be using the terminal window. The bold text is the code you should type in and then execute by pressing ENTER. Most of the examples in this book are small, self-contained programs that you’ll run from your text editor rather than the terminal, because you’ll write most of your code in the text editor. But sometimes, basic concepts will be shown in a series of snippets run through a Python terminal session to demonstrate particular concepts more efficiently. When you see three angle brackets in a code listing, you’re looking at code and output from a terminal session. We’ll try coding in the interpreter on your system in a moment. We’ll also use a text editor to create a simple program called *Hello World!* that has become a staple of learning to program. There’s a long-held tradition in the programming world that printing the message `Hello world!` to the screen as your first program in a new language will bring you good luck. Such a simple program serves a very real purpose. If it runs correctly on your system, then any Python program you write should work as well. ## About the VS Code Editor *VS Code* is a powerful, professional-quality text editor that’s free and beginner-friendly. VS Code is great for both simple and complex projects, so if you become comfortable using it while learning Python, you can continue using it as you progress to larger and more complicated projects. VS Code can be installed on all modern operating systems, and it supports most programming languages, including Python. Appendix B provides information on other text editors. If you’re curious about the other options, you might want to skim that appendix at this point. If you want to begin programming quickly, you can use VS Code to start. Then you can consider other editors, once you’ve gained some experience as a programmer. In this chapter, I’ll walk you through installing VS Code on your operating system. ## Python on Different Operating Systems Python is a cross-platform programming language, which means it runs on all the major operating systems. Any Python program you write should run on any modern computer that has Python installed. However, the methods for setting up Python on different operating systems vary slightly. In this section, you’ll learn how to set up Python on your system. You’ll first check whether a recent version of Python is installed on your system, and install it if it’s not. Then you’ll install VS Code. These are the only two steps that are different for each operating system. In the sections that follow, you’ll run *hello\_world.py* and troubleshoot anything that doesn’t work. I’ll walk you through this process for each operating system, so you’ll have a Python programming environment that you can rely on. ### Python on Windows Windows doesn’t usually come with Python, so you’ll probably need to install it and then install VS Code. #### Installing Python First, check whether Python is installed on your system. Open a command window by entering `command` into the Start menu and clicking the **Command Prompt** app. In the terminal window, enter `python` in lowercase. If you get a Python prompt (`>>>`) in response, Python is installed on your system. If you see an error message telling you that `python` is not a recognized command, or if the Microsoft store opens, Python isn’t installed. Close the Microsoft store if it opens; it’s better to download an official installer than to use Microsoft’s version. If Python is not installed on your system, or if you see a version earlier than Python 3.9, you need to download a Python installer for Windows. Go to [https://python.org](https://python.org/) and hover over the **Downloads** link. You should see a button for downloading the latest version of Python. Click the button, which should automatically start downloading the correct installer for your system. After you’ve downloaded the file, run the installer. Make sure you select the option **Add Python to PATH**, which will make it easier to configure your system correctly. [Figure 1-1](https://learning.oreilly.com/library/view/python-crash-course/9781098156664/c01.xhtml#figure1-1) shows this option selected. ![Make sure you select the checkbox labeled Add Python to PATH. - Hello Python](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/f01001.png?w=640&ssl=1)Make sure you select the checkbox labeled *Add Python to PATH*. #### Running Python in a Terminal Session Open a new command window and enter `python` in lowercase. You should see a Python prompt (`>>>`), which means Windows has found the version of Python you just installed. ``` C:\> python Python 3.x.x (main, Jun . . . , 13:29:14) [MSC v.1932 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> ``` ## **Note** If you don’t see this output or something similar, see the more detailed setup instructions in Appendix A. Enter the following line in your Python session: ``` >>> print("Hello Python interpreter!") Hello Python interpreter! >>> ``` You should see the output `Hello Python interpreter!` Anytime you want to run a snippet of Python code, open a command window and start a Python terminal session. To close the terminal session, press CTRL-Z and then press ENTER, or enter the command `exit()`. #### Installing VS Code You can download an installer for VS Code at [https://code.visualstudio.com](https://code.visualstudio.com/). Click the **Download for Windows** button and run the installer. Skip the following sections about macOS and Linux, and follow the steps in “Running a Hello World Program” on page 9. ### Python on macOS Python is not installed by default on the latest versions of macOS, so you’ll need to install it if you haven’t already done so. In this section, you’ll install the latest version of Python, and then install VS Code and make sure it’s configured correctly. #### Checking Whether Python 3 Is Installed Open a terminal window by going to **Applications**▶**Utilities**▶**Terminal**. You can also press ⌘-spacebar, type `terminal`, and then press ENTER. To see if you have a recent enough version of Python installed, enter `python3`. You’ll most likely see a message about installing the *command line developer tools*. It’s better to install these tools after installing Python, so if this message appears, cancel the pop-up window. If the output shows you have Python 3.9 or a later version installed, you can skip the next section and go to “Running Python in a Terminal Session.” If you see any version earlier than Python 3.9, follow the instructions in the next section to install the latest version. Note that on macOS, whenever you see the `python` command in this book, you need to use the `python3` command instead to make sure you’re using Python 3. On most macOS systems, the `python` command either points to an outdated version of Python that should only be used by internal system tools, or it points to nothing and generates an error message. #### Installing the Latest Version of Python You can find a Python installer for your system at [https://python.org](https://python.org/). Hover over the **Download** link, and you should see a button for downloading the latest version of Python. Click the button, which should automatically start downloading the correct installer for your system. After the file downloads, run the installer. After the installer runs, a Finder window should appear. Double-click the *Install Certificates.command* file. Running this file will allow you to more easily install additional libraries that you’ll need for real-world projects, including the projects in the second half of this book. #### Running Python in a Terminal Session You can now try running snippets of Python code by opening a new terminal window and typing `python3`: ``` $ python3 Python 3.x.x (v3.11.0:eb0004c271, Jun . . . , 10:03:01) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> ``` This command starts a Python terminal session. You should see a Python prompt (`>>>`), which means macOS has found the version of Python you just installed. Enter the following line in the terminal session: ``` >>> print("Hello Python interpreter!") Hello Python interpreter! >>> ``` You should see the message `Hello Python interpreter!`, which should print directly in the current terminal window. You can close the Python interpreter by pressing CTRL-D or by entering the command `exit()`. #### Installing VS Code To install the VS Code editor, you need to download the installer at [https://code.visualstudio.com](https://code.visualstudio.com/). Click the **Download** button, and then open a **Finder** window and go to the **Downloads** folder. Drag the **Visual Studio Code** installer to your Applications folder, then double-click the installer to run it. ### Python on Linux Linux systems are designed for programming, so Python is already installed on most Linux computers. The people who write and maintain Linux expect you to do your own programming at some point, and encourage you to do so. For this reason, there’s very little to install and only a few settings to change to start programming. #### Checking Your Version of Python Open a terminal window by running the Terminal application on your system (in Ubuntu, you can press CTRL-ALT-T). To find out which version of Python is installed, enter `python3` with a lowercase *p*. When Python is installed, this command starts the Python interpreter. You should see output indicating which version of Python is installed. You should also see a Python prompt (`>>>`) where you can start entering Python commands: ``` $ python3 Python 3.10.4 (main, Apr . . . , 09:04:19) [GCC 11.2.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>> ``` This output indicates that Python 3.10.4 is currently the default version of Python installed on this computer. When you’ve seen this output, press CTRL-D or enter `exit()` to leave the Python prompt and return to a terminal prompt. Whenever you see the `python` command in this book, enter `python3` instead. You’ll need Python 3.9 or later to run the code in this book. If the Python version installed on your system is earlier than Python 3.9, or if you want to update to the latest version currently available, refer to the instructions in Appendix A. #### Running Python in a Terminal Session You can try running snippets of Python code by opening a terminal and entering `python3`, as you did when checking your version. Do this again, and when you have Python running, enter the following line in the terminal session: ``` >>> print("Hello Python interpreter!") Hello Python interpreter! >>> ``` The message should print directly in the current terminal window. Remember that you can close the Python interpreter by pressing CTRL-D or by entering the command `exit()`. #### Installing VS Code On Ubuntu Linux, you can install VS Code from the Ubuntu Software Center. Click the Ubuntu Software icon in your menu and search for *vscode*. Click the app called **Visual Studio Code** (sometimes called *code*), and then click **Install**. Once it’s installed, search your system for *VS Code* and launch the app. ## Running a Hello World Program With a recent version of Python and VS Code installed, you’re almost ready to run your first Python program written in a text editor. But before doing so, you need to install the Python extension for VS Code. ### Installing the Python Extension for VS Code VS Code works with many different programming languages; to get the most out of it as a Python programmer, you’ll need to install the Python extension. This extension adds support for writing, editing, and running Python programs. To install the Python extension, click the Manage icon, which looks like a gear in the lower-left corner of the VS Code app. In the menu that appears, click **Extensions**. Enter `python` in the search box and click the **Python** extension. (If you see more than one extension named *Python*, choose the one supplied by Microsoft.) Click **Install** and install any additional tools that your system needs to complete the installation. If you see a message that you need to install Python, and you’ve already done so, you can ignore this message. > If you’re using macOS and a pop-up asks you to install the *command line developer tools*, click **Install**. You may see a message that it will take an excessively long time to install, but it should only take about 10 or 20 minutes on a reasonable internet connection. ### Running hello\_world.py Before you write your first program, make a folder called *python\_work* on your desktop for your projects. It’s best to use lowercase letters and underscores for spaces in file and folder names, because Python uses these naming conventions. You can make this folder somewhere other than the desktop, but it will be easier to follow some later steps if you save the *python\_work* folder directly on your desktop. Open VS Code, and close the **Get Started** tab if it’s still open. Make a new file by clicking **File**▶**New File** or pressing CTRL-N (⌘-N on macOS). Save the file as *hello\_world.py* in your *python\_work* folder. The extension *.py* tells VS Code that your file is written in Python, and tells it how to run the program and highlight the text in a helpful way. After you’ve saved your file, enter the following line in the editor: **hello\_world.py** ``` print("Hello Python world!") ``` To run your program, select **Run**▶**Run Without Debugging** or press CTRL-F5. A terminal screen should appear at the bottom of the VS Code window, showing your program’s output: ``` Hello Python world! ``` You’ll likely see some additional output showing the Python interpreter that was used to run your program. If you want to simplify the information that’s displayed so you only see your program’s output, see Appendix B. You can also find helpful suggestions about how to use VS Code more efficiently in Appendix B. If you don’t see this output, something might have gone wrong in the program. Check every character on the line you entered. Did you accidentally capitalize `print`? Did you forget one or both of the quotation marks or parentheses? Programming languages expect very specific syntax, and if you don’t provide that, you’ll get errors. If you can’t get the program to run, see the suggestions in the next section. ## Troubleshooting If you can’t get *hello\_world.py* to run, here are a few remedies you can try that are also good general solutions for any programming problem: - When a program contains a significant error, Python displays a *traceback*, which is an error report. Python looks through the file and tries to identify the problem. Check the traceback; it might give you a clue as to what issue is preventing the program from running. - Step away from your computer, take a short break, and then try again. Remember that syntax is very important in programming, so something as simple as mismatched quotation marks or mismatched parentheses can prevent a program from running properly. Reread the relevant parts of this chapter, look over your code, and try to find the mistake. - Start over again. You probably don’t need to uninstall any software, but it might make sense to delete your *hello\_world.py* file and re-create it from scratch. - Ask someone else to follow the steps in this chapter, on your computer or a different one, and watch what they do carefully. You might have missed one small step that someone else happens to catch. - See the additional installation instructions in Appendix A; some of the details included in the Appendix may help you solve your issue. - Find someone who knows Python and ask them to help you get set up. If you ask around, you might find that you unexpectedly know someone who uses Python. - The setup instructions in this chapter are also available through this book’s companion website at [https://ehmatthes.github.io/pcc\_3e](https://ehmatthes.github.io/pcc_3e). The online version of these instructions might work better because you can simply cut and paste code and click links to the resources you need. - Ask for help online. Appendix C provides a number of resources, such as forums and live chat sites, where you can ask for solutions from people who’ve already worked through the issue you’re currently facing. Never worry that you’re bothering experienced programmers. Every programmer has been stuck at some point, and most programmers are happy to help you set up your system correctly. As long as you can state clearly what you’re trying to do, what you’ve already tried, and the results you’re getting, there’s a good chance someone will be able to help you. As mentioned in the introduction, the Python community is very friendly and welcoming to beginners. Python should run well on any modern computer. Early setup issues can be frustrating, but they’re well worth sorting out. Once you get *hello\_world.py* running, you can start to learn Python, and your programming work will become more interesting and satisfying. ## Running Python Programs from a Terminal You’ll run most of your programs directly in your text editor. However, sometimes it’s useful to run programs from a terminal instead. For example, you might want to run an existing program without opening it for editing. You can do this on any system with Python installed if you know how to access the directory where the program file is stored. To try this, make sure you’ve saved the *hello\_world.py* file in the *python\_work* folder on your desktop. ### On Windows You can use the terminal command `cd`, for *change directory*, to navigate through your filesystem in a command window. The command `dir`, for *directory*, shows you all the files that exist in the current directory. Open a new terminal window and enter the following commands to run *hello\_world.py*: ``` C:\> cd Desktop\python_work C:\Desktop\python_work> dir hello_world.py C:\Desktop\python_work> python hello_world.py Hello Python world! ``` First, use the `cd` command to navigate to the *python\_work* folder, which is in the *Desktop* folder. Next, use the `dir` command to make sure *hello\_world.py* is in this folder. Then run the file using the command `python hello_world.py`. Most of your programs will run fine directly from your editor. However, as your work becomes more complex, you’ll want to run some of your programs from a terminal. ### On macOS and Linux Running a Python program from a terminal session is the same on Linux and macOS. You can use the terminal command `cd`, for *change directory*, to navigate through your filesystem in a terminal session. The command `ls`, for *list*, shows you all the nonhidden files that exist in the current directory. Open a new terminal window and enter the following commands to run *hello\_world.py*: ``` ~$ cd Desktop/python_work/ ~/Desktop/python_work$ ls hello_world.py ~/Desktop/python_work$ python3 hello_world.py Hello Python world! ``` First, use the `cd` command to navigate to the *python\_work* folder, which is in the *Desktop* folder. Next, use the `ls` command to make sure *hello\_world.py* is in this folder. Then run the file using the command `python3 hello_world.py`. Most of your programs will run fine directly from your editor. But as your work becomes more complex, you’ll want to run some of your programs from a terminal. **Categories:** Python **Tags:** python **Hashtags:** python --- ### [Introduction to Node.js](https://puresourcecode.com/javascript/introduction-to-node-js/) **Published:** October 12, 2024 **Author:** Enrico Rossini **Excerpt:** This is an introduction to Node.js that is a server to run JavaScript apps. After a brief history, I will create a simple web application. **Content:** This is an introduction to [Node.js](https://nodejs.org/) that is a server to run JavaScript applications. After a brief history of this tools, I will create a simple web application. The source code of this post is available on [GitHub](https://github.com/erossini/BirkbeckCloudComputingCourse). This is part of my [MSc at Birkbeck University](https://www.bbk.ac.uk/courses/postgraduate/advanced-computing). ## What is Node.js? Node.js has long been a staple for server-side programming, offering a JavaScript runtime that enables developers to build scalable network applications. In the latest release, Node.js 21, the platform takes a significant leap forward, introducing updates and features that promise to enhance performance, security, and developer experience. ### What’s new in the latest version One of the most notable changes in Node.js 21 is the update of the V8 JavaScript engine to version 11.8. This update brings improved performance and new language features, such as Array grouping and ArrayBuffer.prototype.transfer, which developers can leverage to write more efficient code. Another exciting development is the stabilization of fetch and WebStreams. These features have been marked stable, indicating their readiness for production use. Fetch provides a straightforward way to make web requests, while WebStreams allows handling streaming data, such as video or audio, with ease. Node.js 21 also introduces an experimental flag –experimental-default-type, which allows developers to flip the default module system used by Node.js. This feature is particularly useful for those who are transitioning from CommonJS to ES modules, as it provides a more flexible way to manage module types. The built-in WebSocket client is another addition that catches the eye. Although still experimental, it offers a browser-compatible WebSocket implementation, enabling real-time communication capabilities for web applications. For those involved in testing, support for globs in the Node.js test runner is a welcome enhancement. It simplifies the process of running tests by allowing the use of glob patterns, making it easier to execute tests across multiple files and directories. Updating to Node.js 21 is straightforward, and developers are encouraged to adopt the new version to take advantage of these improvements. The update process typically involves clearing the NPM cache, installing the new version, and, if necessary, pruning previous versions to maintain a clean development environment. As Node.js continues to evolve, it remains committed to providing a robust and versatile platform for server-side development. Node.js 21 is a testament to this commitment, offering a range of features that cater to the needs of modern web development. ### For developers For developers looking to stay ahead of the curve, embracing Node.js 21 is a step towards building more performant, secure, and efficient web applications. The future of server-side JavaScript looks bright, and Node.js is at the forefront of this exciting journey. To download Node.js 21 and explore the full list of features and updates, visit the official Node.js website. Embrace the future of server-side JavaScript with Node.js 21 and unlock the full potential of your web applications. ## Install Node.js The first step is to install on our computer Node.js. You can download it from the official website. After the installation, you can use tools like `npm` and run your server for your application. ![Node.js home page - Introduction to Node.js](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-3.png?resize=640%2C313&ssl=1)Node.js home page If you don’t have [Visual Studio Code](https://code.visualstudio.com/), download it from the official website and then you will be ready to start. ![Visual Studio Code main website - Introduction to Node.js](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-4.png?resize=640%2C356&ssl=1)Visual Studio Code main website ## First web application Now, open Visual Studio Code and open a folder from the menu where you add all the files for the first application. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-5.png?resize=640%2C423&ssl=1) First, I have to initialize my Node.js application and for that, I have to open the **Terminal** from Visual Studio Code. ![Open Terminal in Visual Studio Code](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-6.png?resize=640%2C364&ssl=1)Open Terminal in Visual Studio Code Next, in the Terminal window, I have to type the command to initialize the Node.js application: the initialization downloads for us all the required packages. I have to use **NPM** (**Node Package Manager**) and the command is ``` npm init ``` This command prepares for me the basic configuration of a Nade.js project. Press Enter to choose the default values during the initialization like in the following screenshot. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/Nodejs1.gif?resize=640%2C152&ssl=1) The result is a json file called `package.json` with all the details of my application. ``` { "name": "00-install", "version": "1.0.0", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "description": "" } ``` In Visual Studio Code, I can see there is a gray text for Debug. I don’t think it is doing nothing now, but I like to press it and see what it happens. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/Nodejs2.gif?resize=640%2C422&ssl=1) Now, I want to install a package called Express which is a small package provided by Node.js to create a simple and fast server. ``` npm install express ``` Then, I want to install another package called **nodemon** that helps me to restart my server every thing I change my code. ``` npm install nodemon ``` The next step is to change the configuration to use nodemon to run the application. For that, in the `package.json`, remove the line started with `test` and replace with this one ``` "start": "nodemon app.js" ``` By convention, all the Node.js applications start from an `app.js` file. ## The first app.js Now, add in your folder a file called `app.js`. The first thing is to add the library `express` that helps us to have routes and other component in the app. This library is required. ``` const express = require('express') ``` Then, I have to create the application. For that, I type ``` const app = express() ``` So, I can create routes. *Routing* refers to how an application’s endpoints (URIs) respond to client requests. For an introduction to routing, see [Basic routing](https://expressjs.com/en/starter/basic-routing.html). You define routing using methods of the Express `app` object that correspond to HTTP methods; for example, `app.get()` to handle GET requests and `app.post` to handle POST requests. For a full list, see [app.METHOD](https://expressjs.com/en/4x/api.html#app.METHOD). You can also use [app.all()](https://expressjs.com/en/4x/api.html#app.all) to handle all HTTP methods and [app.use()](https://expressjs.com/en/4x/api.html#app.use) to specify middleware as the callback function (See [Using middleware](https://expressjs.com/en/guide/using-middleware.html) for details). These routing methods specify a callback function (sometimes called “handler functions”) called when the application receives a request to the specified route (endpoint) and HTTP method. In other words, the application “listens” for requests that match the specified route(s) and method(s), and when it detects a match, it calls the specified callback function. ### First GET When we type in the browser a URL, this is a **GET** call to the server. Every call has a **request** and a **response**. So, I have to define a simple response to the request to the server as the home page. Add this code: ``` app.get('/', (req, res) => { res.send('Hello!'); }) ``` #### Explanation I created a variable for the application called `app`. To the `app`, I add a GET request to the home page or root of the web application and this is the `'/'` in the first part of the definition of the `GET`. Then, this function requires a response and a result variables that we can use in our code: `req` and `res` are the 2 variables I defined in the core above. The variable `res` has properties and methods that I can use to send output or read input. In this example, I want to print `Hello!` ### Create the server Now, I have to tell Node.js on what port the application has to use to respond to the calls. Generally speaking, a website responds on the port `80` (HTTP) or 443 (HTTPS) but those ports can change. As a convention, I use the port 3000 for Node.js applications. ``` app.listen(3000, () => { console.log('Server is up and running') }) ``` When the application starts, I want to print in the logs a sentence – in this case Server is up and running – for debug purposes. Because I run the application on my local machine, I open the web application using this URL ``` http://localhost:3000/ ``` `localhost` is a conventional name to use in URLs to identify my local machine and this is an alias of the `127.0.0.1` IP that identifies my local machine. ## Debug time Now, I can tell Visual Studio Code that I want to debug this application and try it in a browser. For that, click on the **Run and Debug** on the menu on the left, select the URL ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/Nodejs3.gif?resize=640%2C388&ssl=1) Now, every time I run the application, I can see the result and if there is an error, I will be redirect to the line when the error occurs. Instead of using Visual Studio Code feature to run your code, you can type in the Terminal ``` npm start ``` and the application will start and then you can point your browse to the address `http://localhost:3000`. ## Add a new route Just for fun, I add another route in the application adding this code ``` app.get('/movies', (req, res) => { res.send('You are in the movies page') }) ``` If I run the application, I can see the home page but also I can go to ``` http://localhost:3000/movies ``` ## Route folder Now, to keep your application easy to read and understand, a common action to do is to have different folders where to add the files for each section of the application. For this reason, create a new folder called `routes`. ## The route for movies Now, I create a file called `movies.js` that is another `express` application. Because this is a new route, I have to define that for Node.js and so, I have to write those lines ``` const express = require('express') const router = express.Router() ``` In order to let Node.js knows that I want to use the routes in this file, I have to export the routes with there definitions. So, I added at the end of the file this line ``` module.exports = router ``` Adding this route means that Node.js recognizes an URL like `http://localhost:3000/movies` Then, I can add the routes I like and also I can define different routes in this file. For example, adding those lines ``` router.get('/', (req, res) => { res.send('You are in movies (router)') }) ``` allows us to open the URL `http://localhost:3000/movies` because this code defines the root of the route `movies`. If I want to add another sub URL, for example `http://localhost:3000/movies/starwars`, I have to write another router like ``` router.get('/starwars', (req, res) => { res.send('Star Wars!') }) ``` ## Add the route to the application Now, define the route and the router is not enough for the application to know where to do. So, I have to add this route to the main application. So, in the `app.js` I have to tell that I want to use the file movies.js and that I want to map the routes in this file with the route `movies`. Here the code: ``` const movieRoute = require('./routes/movies') // Middleware app.use('/movies', movieRoute) ``` We call **middleware** this implementation of the routes. Now, we can play with the application. ## Wrap up I hope this is a good and simple introduction to Node.js. Remember, you have all the source code in [GitHub](https://github.com/erossini/BirkbeckCloudComputingCourse). **Categories:** JavaScript, Node.js **Tags:** javascript, node.js **Hashtags:** javascript, node.js --- ### [How to use FlexLayout with different sizes](https://puresourcecode.com/dotnet/csharp/how-to-use-flexlayout-with-different-sizes/) **Published:** October 11, 2024 **Author:** Enrico Rossini **Excerpt:** In this new post, I want to show you via a simple project how to use in MAUI FlexLayout with children with different sizes. **Content:** In this new post, I want to show you via a simple project how to use in [MAUI](https://puresourcecode.com/tag/maui/) **FlexLayout** with children with different sizes. FlexLayout in MAUI is a layout that can arrange its children horizontally and vertically in a stack. Also, it wraps its children if there are too many children to fit in a single row or column. FlexLayout can control orientation and alignment, and adapt to different screen sizes. The source code of this post is available on [GitHub](https://github.com/erossini/MAUIFlexSkills). ## Create the value converters ### StringToViewSizeStringConverter Now, the first converter will help me with the [FlexBasis](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/layouts/flexlayout?view=net-maui-8.0) which is an attached property that defines the initial main size of the child before free space is distributed according to other property values. The default value of this property is `Auto`. If the **Entry** text length is more than 1 then this converter will return 90% of **FlexBasis** else 100% **FlexBasis**. ``` public class StringToViewSizeStringConverter : IValueConverter { public StringToViewSizeStringConverter() { } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { string val = (string)value; if (!string.IsNullOrEmpty(val)) { return new FlexBasis(0.90f, true); } else { return new FlexBasis(1f, true); } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return !((bool)value); } } ``` ### StringToReverseBoolConverter I want to apply this converter to the Entry in the UI. If the Entry has text (text length more than 1) then I want to display an icon. Here the simple implementation for checking the length of the Entry. ``` public class StringToReverseBoolConverter : IValueConverter { public StringToReverseBoolConverter() { } public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { string val = (string)value; if (!string.IsNullOrEmpty(val)) { if (val.Length > 0) { return true; } else { return false; } } else { return false; } } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return !((bool)value); } } ``` ## The MainPage Finally, I have to create the XAML to display the skills using a view model to manage the skills. ## ViewModel Now, the view model is quite simple. The property `Skills` is collecting all the values the UI has to display. ``` using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MAUIFlexSkills.ViewModels { public class MainPageViewModel { private ObservableCollection? _skills; public MainPageViewModel() { Skills = new ObservableCollection(); } public ObservableCollection? Skills { get { return _skills; } set { _skills = value; } } } } ``` ## XAML Now, we can use those converters in the MainPage. So, add the reference to them and I call it `converter`. Then, I call each of them, ready to be used. ``` ``` ### Entry part Then, you can start to add the Entry in a FlexLayout. When the user starts to type, an image of a check will be shown. If the user clicks on this image, the skill will be added to the list. ``` ``` The result is as in the following screenshot, with and without a text value in the Entry. ![Entry without text - How to use FlexLayout with different sizes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image.png?resize=640%2C64&ssl=1)Entry without text ![Entry with a value and the check - How to use FlexLayout with different sizes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-1.png?resize=640%2C71&ssl=1)Entry with a value and the check 1. **Direction=”Row**“: Children will appear horizontally. 2. For **Entry**, I have used **StringToViewSizeStringConverter** converter to manage the width size. ### Display the skills Then, this is the interesting part. Here I display the label for each skill and each of them has a different size. ``` ``` I have used the properties in **FlexLayout** as follows: - **FlexLayout.Basis** attached property to its children. - **FlexLayout.Basis**, defines the amount of space that’s allocated to a child on the main axis. - **JustifyContent=**“**Start”**: children will aligned from the start without any spacing. - **Wrap=”Wrap”**: **FlexLayout** will wrap its children. If there are too many children to fit in a single row. ![FlexLayout Skill output example](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/image-2.png?resize=640%2C303&ssl=1)FlexLayout Skill output example ## Wrap up In conclusion, this is how to use FlexLayout with different sizes with a simple working example. Please contact me if you have any question or improvement. **Categories:** .NET8, C#, MAUI **Tags:** maui, net8, skills, ui **Hashtags:** maui, net8 --- ### [Build your sentence in MAUI](https://puresourcecode.com/dotnet/maui/build-your-sentence-in-maui/) **Published:** October 10, 2024 **Author:** Enrico Rossini **Excerpt:** How to create a simple component to build your sentence in MAUI using taps and gestures. Source code available. **Content:** I’m creating on app for helping people to learn [languages](https://puresourcecode.com/news/language-in-use-is-here/) called [LanguageInUse](https://languageinuse.com/) and I want to build a page where you can put together your sentence using MAUI. As a result, I like to have a component to reuse in my app. The source code is available on [GitHub](https://github.com/erossini/MAUISentenceBuilder). ## Setup the project First, my goal is to create an interactive component in .NET MAUI where users can tap on words to form a sentence, which is a fun and engaging task. Here’s a basic example to get you started: 1. **Set up the project**: ensure you have a .NET MAUI project set up in Visual Studio. 2. **Create the UI**: Define a `StackLayout` for the words and a `Label` to display the formed sentence. 3. **Handle the word taps**: Use `Button` controls for the words and handle their `Clicked` events to update the sentence. After creating a new MAUI project, I’m going to replace the `MainPage` with another `ContentPage` using C# (not XAML). ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private Label sentenceLabel; private string formedSentence = ""; public MainPage() { sentenceLabel = new Label { Text = "Formed Sentence: ", FontSize = 24, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start }; var words = new[] { "Hello", "world", "this", "is", "MAUI" }; var wordButtons = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; foreach (var word in words) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnWordButtonClicked; wordButtons.Children.Add(button); } Content = new StackLayout { Children = { sentenceLabel, wordButtons } }; } private void OnWordButtonClicked(object sender, EventArgs e) { if (sender is Button button) { formedSentence += button.Text + " "; sentenceLabel.Text = "Formed Sentence: " + formedSentence; } } } } ``` ![First example - Build your sentence in MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder1.gif?resize=640%2C113&ssl=1)The first run of the component ### Explanation - **Label**: Displays the formed sentence. - **StackLayout**: Holds the word buttons at the bottom of the screen. - **Button Click Event**: Appends the clicked word to the sentence and updates the label. This example provides a basic structure. You can expand it by adding features like clearing the sentence, rearranging words, or validating the formed sentence. ## Move the selected words Now, I like to see the selected words below the list of words. So, let’s modify the example to display the selected words below the list of words. ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private Label sentenceLabel; private string formedSentence = ""; public MainPage() { sentenceLabel = new Label { Text = "Formed Sentence: ", FontSize = 24, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End }; var words = new[] { "Hello", "world", "this", "is", "MAUI" }; var wordButtons = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; foreach (var word in words) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnWordButtonClicked; wordButtons.Children.Add(button); } Content = new StackLayout { Children = { wordButtons, sentenceLabel } }; } private void OnWordButtonClicked(object sender, EventArgs e) { if (sender is Button button) { formedSentence += button.Text + " "; sentenceLabel.Text = "Formed Sentence: " + formedSentence; } } } } ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder2.gif?resize=640%2C113&ssl=1) ### Explanation - **Label**: Displays the formed sentence and is positioned below the word buttons. - **StackLayout**: The `wordButtons` layout is placed at the top, and the `sentenceLabel` is placed below it. Now, the selected words will appear below the list of words. This is making easier for users to see the sentence they are forming. ## Display words as buttons Next step, I want to see the selected words below as a button. Once a word is selected, it has to disappear from the top list and appear at the bottom. If the user clicks on a word in the bottom list, it will disappear and reappear in the list at the top Let’s create a more interactive component where selected words move between two lists. 1. **Define the UI**: We’ll have two `StackLayout`s, one for the available words and one for the selected words. 2. **Handle word selection and deselection**: We’ll update the lists dynamically based on user interactions. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List availableWords; private List selectedWords; public MainPage() { availableWords = new List { "Hello", "world", "this", "is", "MAUI" }; selectedWords = new List(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; UpdateWordButtons(); Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout } }; } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in availableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } } private void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { availableWords.Remove(button.Text); selectedWords.Add(button.Text); UpdateWordButtons(); } } private void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { selectedWords.Remove(button.Text); availableWords.Add(button.Text); UpdateWordButtons(); } } } } ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder3.gif?resize=640%2C113&ssl=1) ### Explanation - **Two `StackLayout`s**: One for available words and one for selected words. - **Dynamic Updates**: The `UpdateWordButtons` method updates the buttons in both layouts based on the current state of the lists. - **Event Handlers**: `OnAvailableWordClicked` moves a word from the available list to the selected list, and `OnSelectedWordClicked` moves it back. This setup allows users to interactively build and modify their sentence by moving words between the two lists. ## Add animation Now, when the user taps on a button, I like to see an animation that moves the button from to top list to the bottom list or vice-versa. Adding animations to move buttons between the lists will make the interaction more engaging. Here’s how you can implement this in .NET MAUI: 1. **Define the animations**: We’ll use `TranslateTo` for moving the buttons. 2. **Update the event handlers**: Incorporate the animations when buttons are clicked. Here’s the updated code: ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List availableWords; private List selectedWords; public MainPage() { availableWords = new List { "Hello", "world", "this", "is", "MAUI" }; selectedWords = new List(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; UpdateWordButtons(); Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout } }; } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in availableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { await AnimateButton(button, availableWordsLayout, selectedWordsLayout); availableWords.Remove(button.Text); selectedWords.Add(button.Text); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { await AnimateButton(button, selectedWordsLayout, availableWordsLayout); selectedWords.Remove(button.Text); availableWords.Add(button.Text); UpdateWordButtons(); } } private async Task AnimateButton(Button button, StackLayout fromLayout, StackLayout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } } } ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder4.gif?resize=640%2C113&ssl=1) ### Explanation - **AnimateButton Method**: This method handles the animation of moving the button from one layout to another. - **Initial and Final Positions**: Calculate the initial and final positions of the button. - **Translation**: Set the button’s translation to move it from the initial to the final position. - **TranslateTo**: Animate the button to its new position over 500 milliseconds using a cubic easing function. This setup will animate the buttons as they move between the available and selected word lists, providing a smooth and visually appealing transition. ## Add placeholder Now, when a button is clicked and moved in the other list on the bottom, I like to see a placeholder for this button. If a button in the bottom part is clicked, the button has to return in the right placeholder on the top. When a button is clicked and moved to the other list, a placeholder will appear in its original position. If a button in the bottom part is clicked, it will return to its original placeholder at the top. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List availableWords; private List selectedWords; private Dictionary placeholders; public MainPage() { availableWords = new List { "Hello", "world", "this", "is", "MAUI" }; selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; UpdateWordButtons(); Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout } }; } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in availableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); var placeholder = new BoxView { Color = Colors.Gray, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; placeholders[word] = placeholder; } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } foreach (var word in placeholders.Keys) { if (!availableWords.Contains(word)) { availableWordsLayout.Children.Add(placeholders[word]); } } } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { availableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { selectedWords.Remove(button.Text); availableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } } } ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder5.gif?resize=640%2C113&ssl=1) ### Explanation - **Placeholders Dictionary**: A dictionary to store placeholders for each word. - **BoxView as Placeholders**: Gray `BoxView` elements act as placeholders. - **UpdateWordButtons Method**: Adds placeholders to the layout when a word is moved. - **AnimateButton Method**: Animates the button to its new position. This setup will ensure that placeholders appear in the original positions of the buttons, and buttons can return to their placeholders when clicked. ## Placeholder improvement The placeholder has to be displayed in the same position as the selected button Let’s update the code so that the placeholder appears in the same position as the selected button. When a button is clicked and moved to the other list, a placeholder will appear in its original position. If a button in the bottom part is clicked, it will return to its original placeholder at the top. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List availableWords; private List selectedWords; private Dictionary placeholders; public MainPage() { availableWords = new List { "Hello", "world", "this", "is", "MAUI" }; selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; UpdateWordButtons(); Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout } }; } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in availableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = Colors.Gray, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; placeholders[word] = placeholder; } } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } foreach (var word in placeholders.Keys) { if (!availableWords.Contains(word)) { availableWordsLayout.Children.Add(placeholders[word]); } } } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { availableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { selectedWords.Remove(button.Text); availableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } } } ``` ### Explanation - **Placeholders Dictionary**: A dictionary to store placeholders for each word. - **BoxView as Placeholders**: Gray `BoxView` elements act as placeholders. - **UpdateWordButtons Method**: Adds placeholders to the layout when a word is moved. - **AnimateButton Method**: Animates the button to its new position. This setup will ensure that placeholders appear in the original positions of the buttons, and buttons can return to their placeholders when clicked. ## Validate the sentence After the user selects at least one of the buttons, a button has to appear to validate the sentence. If the selected words are the same of the available words, the user gives a correct answer. Let’s add a validation button that appears once the user selects at least one word. This button will check if the selected words form the correct sentence. Here’s how you can implement this: 1. **Add a validation button**: The button will be initially hidden and will appear when at least one word is selected. 2. **Validate the sentence**: Check if the selected words match the original list of available words. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List availableWords; private List selectedWords; private Dictionary placeholders; private Button validateButton; public MainPage() { availableWords = new List { "Hello", "world", "this", "is", "MAUI" }; selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; validateButton = new Button { Text = "Validate Sentence", FontSize = 18, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, IsVisible = false }; validateButton.Clicked += OnValidateButtonClicked; UpdateWordButtons(); Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout, validateButton } }; } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in availableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = Colors.Gray, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; placeholders[word] = placeholder; } } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } foreach (var word in placeholders.Keys) { if (!availableWords.Contains(word)) { availableWordsLayout.Children.Add(placeholders[word]); } } validateButton.IsVisible = selectedWords.Any(); } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { availableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { selectedWords.Remove(button.Text); availableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } private void OnValidateButtonClicked(object sender, EventArgs e) { if (selectedWords.SequenceEqual(availableWords)) { DisplayAlert("Correct!", "You have formed the correct sentence.", "OK"); } else { DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK"); } } } } ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder6.gif?resize=640%2C348&ssl=1)### Explanation - **Validation Button**: A button that appears when at least one word is selected. - **Validation Logic**: Checks if the selected words match the original list of available words. - **UpdateWordButtons Method**: Updates the visibility of the validation button based on the selection. This setup will allow users to validate their sentence and receive feedback on whether it is correct. ## Convert to a component Let’s create a reusable .NET MAUI component where `availableWords` is a parameter. This component will encapsulate the functionality and allow you to pass different sets of words. Here’s how you can define the component: 1. **Create a new class for the component**: This class will inherit from `ContentView`. 2. **Define the `availableWords` parameter**: Use a bindable property to allow setting the words from outside the component. 3. **Implement the component logic**: Include the UI and interaction logic within the component. Here’s the code for the `SentenceBuilder` component: ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class SentenceBuilder : ContentView { public static readonly BindableProperty AvailableWordsProperty = BindableProperty.Create(nameof(AvailableWords), typeof(List), typeof(SentenceBuilder), new List(), propertyChanged: OnAvailableWordsChanged); public List AvailableWords { get => (List)GetValue(AvailableWordsProperty); set => SetValue(AvailableWordsProperty, value); } private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List selectedWords; private Dictionary placeholders; private Button validateButton; public SentenceBuilder() { selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; validateButton = new Button { Text = "Validate Sentence", FontSize = 18, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, IsVisible = false }; validateButton.Clicked += OnValidateButtonClicked; Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout, validateButton } }; UpdateWordButtons(); } private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue) { var control = (SentenceBuilder)bindable; control.UpdateWordButtons(); } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in AvailableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = Colors.Gray, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; placeholders[word] = placeholder; } } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } foreach (var word in placeholders.Keys) { if (!AvailableWords.Contains(word)) { availableWordsLayout.Children.Add(placeholders[word]); } } validateButton.IsVisible = selectedWords.Any(); } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { AvailableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { selectedWords.Remove(button.Text); AvailableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } private void OnValidateButtonClicked(object sender, EventArgs e) { if (selectedWords.SequenceEqual(AvailableWords)) { //DisplayAlert("Correct!", "You have formed the correct sentence.", "OK"); } else { //DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK"); } } } } ``` ### Explanation - **Bindable Property**: `AvailableWords` is defined as a bindable property to allow setting it from outside the component. - **Component Logic**: The logic for handling word selection, deselection, and validation is encapsulated within the `SentenceBuilder` class. - **UI Layout**: The component’s UI is defined within the `ContentView`. ### Usage Example: To use this component in your main page, you can set the `AvailableWords` property like this: ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { public MainPage() { var sentenceBuilder = new SentenceBuilder { AvailableWords = new List { "Hello", "world", "this", "is", "MAUI" } }; Content = new StackLayout { Children = { sentenceBuilder } }; } } } ``` This setup makes the `SentenceBuilder` component reusable with different sets of words. ## Add events So, from the component is not possible to use `DisplayAlert`. Now, I want to change the code in this way. `OnValidateButtonClicked` has to raise an event to return true or false and remove the `DisplayAlert`. Let’s modify the `SentenceBuilder` component to raise an event instead of displaying an alert. This event will return `true` if the sentence is correct and `false` otherwise. Here’s the updated `SentenceBuilder` component: ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class SentenceBuilder : ContentView { public static readonly BindableProperty AvailableWordsProperty = BindableProperty.Create(nameof(AvailableWords), typeof(List), typeof(SentenceBuilder), new List(), propertyChanged: OnAvailableWordsChanged); public List AvailableWords { get => (List)GetValue(AvailableWordsProperty); set => SetValue(AvailableWordsProperty, value); } public event EventHandler SentenceValidated; private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List selectedWords; private Dictionary placeholders; private Button validateButton; public SentenceBuilder() { selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; validateButton = new Button { Text = "Validate Sentence", FontSize = 18, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, IsVisible = false }; validateButton.Clicked += OnValidateButtonClicked; Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout, validateButton } }; UpdateWordButtons(); } private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue) { var control = (SentenceBuilder)bindable; control.UpdateWordButtons(); } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in AvailableWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = Colors.Gray, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center }; placeholders[word] = placeholder; } } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = 18 }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } foreach (var word in placeholders.Keys) { if (!AvailableWords.Contains(word)) { availableWordsLayout.Children.Add(placeholders[word]); } } validateButton.IsVisible = selectedWords.Any(); } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { AvailableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { selectedWords.Remove(button.Text); AvailableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } private void OnValidateButtonClicked(object sender, EventArgs e) { bool isCorrect = selectedWords.SequenceEqual(AvailableWords); SentenceValidated?.Invoke(this, isCorrect); } } } ``` ### Explanation - **Event Declaration**: `SentenceValidated` event is declared to notify when the sentence is validated. - **Event Invocation**: The `OnValidateButtonClicked` method raises the `SentenceValidated` event with `true` if the sentence is correct and `false` otherwise. ### Usage Example To use this component and handle the validation event on your main page: ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { public MainPage() { var sentenceBuilder = new SentenceBuilder { AvailableWords = new List { "Hello", "world", "this", "is", "MAUI" } }; sentenceBuilder.SentenceValidated += OnSentenceValidated; Content = new StackLayout { Children = { sentenceBuilder } }; } private void OnSentenceValidated(object sender, bool isCorrect) { if (isCorrect) { DisplayAlert("Correct!", "You have formed the correct sentence.", "OK"); } else { DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK"); } } } } ``` This setup makes the `SentenceBuilder` component reusable and allows you to handle the validation result on your main page. ## Add more properties to the component The next step is to add some properties to the component such as the color of the button and placeholder, the font and the text size. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class SentenceBuilder : ContentView { public static readonly BindableProperty AvailableWordsProperty = BindableProperty.Create(nameof(AvailableWords), typeof(List), typeof(SentenceBuilder), new List(), propertyChanged: OnAvailableWordsChanged); public static readonly BindableProperty ButtonColorProperty = BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(SentenceBuilder), Colors.Blue); public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(SentenceBuilder), Colors.Gray); public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SentenceBuilder), "Arial"); public static readonly BindableProperty TextSizeProperty = BindableProperty.Create(nameof(TextSize), typeof(double), typeof(SentenceBuilder), 18.0); public List AvailableWords { get => (List)GetValue(AvailableWordsProperty); set => SetValue(AvailableWordsProperty, value); } public Color ButtonColor { get => (Color)GetValue(ButtonColorProperty); set => SetValue(ButtonColorProperty, value); } public Color PlaceholderColor { get => (Color)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, value); } public string FontFamily { get => (string)GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } public double TextSize { get => (double)GetValue(TextSizeProperty); set => SetValue(TextSizeProperty, value); } public event EventHandler SentenceValidated; private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List selectedWords; private Dictionary placeholders; private Button validateButton; public SentenceBuilder() { selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; validateButton = new Button { Text = "Validate Sentence", FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, IsVisible = false }; validateButton.Clicked += OnValidateButtonClicked; Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout, validateButton } }; UpdateWordButtons(); } private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue) { var control = (SentenceBuilder)bindable; control.UpdateWordButtons(); } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in AvailableWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = PlaceholderColor, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, IsVisible = false }; placeholders[word] = placeholder; } availableWordsLayout.Children.Add(placeholders[word]); } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } validateButton.IsVisible = selectedWords.Any(); } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { placeholders[button.Text].IsVisible = true; AvailableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { placeholders[button.Text].IsVisible = false; selectedWords.Remove(button.Text); AvailableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } private void OnValidateButtonClicked(object sender, EventArgs e) { bool isCorrect = selectedWords.SequenceEqual(AvailableWords); SentenceValidated?.Invoke(this, isCorrect); } } } ``` ### Explanation - **Bindable Properties**: Added properties for `ButtonColor`, `PlaceholderColor`, `FontFamily`, and `TextSize` with default values. - **Customization**: The button and placeholder colors, font family, and text size can now be customized through these properties. ### Usage Example To use this component and customize the appearance of your main page. ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { public MainPage() { var sentenceBuilder = new SentenceBuilder { AvailableWords = new List { "Hello", "world", "this", "is", "MAUI" }, ButtonColor = Colors.Green, PlaceholderColor = Colors.LightGray, FontFamily = "Helvetica", TextSize = 20 }; sentenceBuilder.SentenceValidated += OnSentenceValidated; Content = new StackLayout { Children = { sentenceBuilder } }; } private void OnSentenceValidated(object sender, bool isCorrect) { if (isCorrect) { DisplayAlert("Correct!", "You have formed the correct sentence.", "OK"); } else { DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK"); } } } } ``` ## Enable the validation from the MainPage Now, I like to have an event when it is possible to validate a sentence. The validation can be triggered from the `MainPage`. Let’s add an event that notifies when it is possible to validate a sentence. This event will be triggered whenever the selected words list is updated, allowing the main page to handle the validation logic. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class SentenceBuilder : ContentView { public static readonly BindableProperty AvailableWordsProperty = BindableProperty.Create(nameof(AvailableWords), typeof(List), typeof(SentenceBuilder), new List(), propertyChanged: OnAvailableWordsChanged); public static readonly BindableProperty ButtonColorProperty = BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(SentenceBuilder), Colors.Blue); public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(SentenceBuilder), Colors.Gray); public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SentenceBuilder), "Arial"); public static readonly BindableProperty TextSizeProperty = BindableProperty.Create(nameof(TextSize), typeof(double), typeof(SentenceBuilder), 18.0); public List AvailableWords { get => (List)GetValue(AvailableWordsProperty); set => SetValue(AvailableWordsProperty, value); } public Color ButtonColor { get => (Color)GetValue(ButtonColorProperty); set => SetValue(ButtonColorProperty, value); } public Color PlaceholderColor { get => (Color)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, value); } public string FontFamily { get => (string)GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } public double TextSize { get => (double)GetValue(TextSizeProperty); set => SetValue(TextSizeProperty, value); } public event EventHandler SentenceValidated; public event EventHandler CanValidateSentenceChanged; private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List selectedWords; private Dictionary placeholders; private Button validateButton; public SentenceBuilder() { selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; validateButton = new Button { Text = "Validate Sentence", FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, IsVisible = false }; validateButton.Clicked += OnValidateButtonClicked; Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout, validateButton } }; UpdateWordButtons(); } private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue) { var control = (SentenceBuilder)bindable; control.UpdateWordButtons(); } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in AvailableWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = PlaceholderColor, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, IsVisible = false }; placeholders[word] = placeholder; } availableWordsLayout.Children.Add(placeholders[word]); } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } validateButton.IsVisible = selectedWords.Any(); CanValidateSentenceChanged?.Invoke(this, EventArgs.Empty); } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { placeholders[button.Text].IsVisible = true; AvailableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { placeholders[button.Text].IsVisible = false; selectedWords.Remove(button.Text); AvailableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, 500, Easing.CubicInOut); } private void OnValidateButtonClicked(object sender, EventArgs e) { bool isCorrect = selectedWords.SequenceEqual(AvailableWords); SentenceValidated?.Invoke(this, isCorrect); } } } ``` ### Explanation - **CanValidateSentenceChanged Event**: This event is triggered whenever the selected words list is updated, indicating that the sentence can be validated. - **UpdateWordButtons Method**: Invokes the `CanValidateSentenceChanged` event to notify the main page. ### Usage Example To use this component and handle the validation event on your main page: ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private SentenceBuilder sentenceBuilder; public MainPage() { sentenceBuilder = new SentenceBuilder { AvailableWords = new List { "Hello", "world", "this", "is", "MAUI" }, ButtonColor = Colors.Green, PlaceholderColor = Colors.LightGray, FontFamily = "Helvetica", TextSize = 20 }; sentenceBuilder.SentenceValidated += OnSentenceValidated; sentenceBuilder.CanValidateSentenceChanged += OnCanValidateSentenceChanged; var validateButton = new Button { Text = "Validate", FontSize = 18, IsVisible = false }; validateButton.Clicked += (sender, e) => sentenceBuilder.OnValidateButtonClicked(sender, e); Content = new StackLayout { Children = { sentenceBuilder, validateButton } }; } private void OnSentenceValidated(object sender, bool isCorrect) { if (isCorrect) { DisplayAlert("Correct!", "You have formed the correct sentence.", "OK"); } else { DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK"); } } private void OnCanValidateSentenceChanged(object sender, EventArgs e) { var validateButton = (Button)((StackLayout)Content).Children.Last(); validateButton.IsVisible = sentenceBuilder.SelectedWords.Any(); } } } ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder7.gif?resize=640%2C348&ssl=1) ## Add property for animation duration Now, I can customize the animation duration for button transitions. Let’s add a bindable property to the SentenceBuilder component to allow customization of the animation duration. ``` using Microsoft.Maui.Controls; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace MAUISentenceBuilder { public class SentenceBuilder : ContentView { public static readonly BindableProperty AvailableWordsProperty = BindableProperty.Create(nameof(AvailableWords), typeof(List), typeof(SentenceBuilder), new List(), propertyChanged: OnAvailableWordsChanged); public static readonly BindableProperty ButtonColorProperty = BindableProperty.Create(nameof(ButtonColor), typeof(Color), typeof(SentenceBuilder), Colors.Blue); public static readonly BindableProperty PlaceholderColorProperty = BindableProperty.Create(nameof(PlaceholderColor), typeof(Color), typeof(SentenceBuilder), Colors.Gray); public static readonly BindableProperty FontFamilyProperty = BindableProperty.Create(nameof(FontFamily), typeof(string), typeof(SentenceBuilder), "Arial"); public static readonly BindableProperty TextSizeProperty = BindableProperty.Create(nameof(TextSize), typeof(double), typeof(SentenceBuilder), 18.0); public static readonly BindableProperty AnimationDurationProperty = BindableProperty.Create(nameof(AnimationDuration), typeof(uint), typeof(SentenceBuilder), 500u); public List AvailableWords { get => (List)GetValue(AvailableWordsProperty); set => SetValue(AvailableWordsProperty, value); } public Color ButtonColor { get => (Color)GetValue(ButtonColorProperty); set => SetValue(ButtonColorProperty, value); } public Color PlaceholderColor { get => (Color)GetValue(PlaceholderColorProperty); set => SetValue(PlaceholderColorProperty, value); } public string FontFamily { get => (string)GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } public double TextSize { get => (double)GetValue(TextSizeProperty); set => SetValue(TextSizeProperty, value); } public uint AnimationDuration { get => (uint)GetValue(AnimationDurationProperty); set => SetValue(AnimationDurationProperty, value); } public List SelectedWords => selectedWords; public event EventHandler SentenceValidated; public event EventHandler CanValidateSentenceChanged; private StackLayout availableWordsLayout; private StackLayout selectedWordsLayout; private List selectedWords; private Dictionary placeholders; private Button validateButton; public SentenceBuilder() { selectedWords = new List(); placeholders = new Dictionary(); availableWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Start, Spacing = 10 }; selectedWordsLayout = new StackLayout { Orientation = StackOrientation.Horizontal, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, Spacing = 10 }; validateButton = new Button { Text = "Validate Sentence", FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.End, IsVisible = false }; validateButton.Clicked += OnValidateButtonClicked; Content = new StackLayout { Children = { availableWordsLayout, selectedWordsLayout, validateButton } }; UpdateWordButtons(); } private static void OnAvailableWordsChanged(BindableObject bindable, object oldValue, object newValue) { var control = (SentenceBuilder)bindable; control.UpdateWordButtons(); } private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in AvailableWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnAvailableWordClicked; availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = PlaceholderColor, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, IsVisible = false }; placeholders[word] = placeholder; } availableWordsLayout.Children.Add(placeholders[word]); } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnSelectedWordClicked; selectedWordsLayout.Children.Add(button); } validateButton.IsVisible = selectedWords.Any(); CanValidateSentenceChanged?.Invoke(this, EventArgs.Empty); } private async void OnAvailableWordClicked(object sender, EventArgs e) { if (sender is Button button) { placeholders[button.Text].IsVisible = true; AvailableWords.Remove(button.Text); selectedWords.Add(button.Text); await AnimateButton(button, availableWordsLayout, selectedWordsLayout); UpdateWordButtons(); } } private async void OnSelectedWordClicked(object sender, EventArgs e) { if (sender is Button button) { placeholders[button.Text].IsVisible = false; selectedWords.Remove(button.Text); AvailableWords.Add(button.Text); await AnimateButton(button, selectedWordsLayout, availableWordsLayout); UpdateWordButtons(); } } private async Task AnimateButton(Button button, Layout fromLayout, Layout toLayout) { var initialPosition = button.Bounds; fromLayout.Children.Remove(button); toLayout.Children.Add(button); var finalPosition = button.Bounds; button.TranslationX = initialPosition.X - finalPosition.X; button.TranslationY = initialPosition.Y - finalPosition.Y; await button.TranslateTo(0, 0, AnimationDuration, Easing.CubicInOut); } private void OnValidateButtonClicked(object sender, EventArgs e) { bool isCorrect = selectedWords.SequenceEqual(AvailableWords); SentenceValidated?.Invoke(this, isCorrect); } } } ``` ### Explanation - **AnimationDuration Property**: Added a bindable property `AnimationDuration` with a default value of 500 milliseconds. - **AnimateButton Method**: Uses the `AnimationDuration` property to control the duration of the button transition animation. ### Usage Example To use this component and customize the animation duration in your main page. ``` using Microsoft.Maui.Controls; namespace MAUISentenceBuilder { public class MainPage : ContentPage { private SentenceBuilder sentenceBuilder; public MainPage() { sentenceBuilder = new SentenceBuilder { AvailableWords = new List { "Hello", "world", "this", "is", "MAUI" }, ButtonColor = Colors.Green, PlaceholderColor = Colors.LightGray, FontFamily = "Helvetica", TextSize = 20, AnimationDuration = 1000 // Custom animation duration in milliseconds }; sentenceBuilder.SentenceValidated += OnSentenceValidated; sentenceBuilder.CanValidateSentenceChanged += OnCanValidateSentenceChanged; var validateButton = new Button { Text = "Validate", FontSize = 18, IsVisible = false }; validateButton.Clicked += (sender, e) => sentenceBuilder.OnValidateButtonClicked(sender, e); Content = new StackLayout { Children = { sentenceBuilder, validateButton } }; } private void OnSentenceValidated(object sender, bool isCorrect) { if (isCorrect) { DisplayAlert("Correct!", "You have formed the correct sentence.", "OK"); } else { DisplayAlert("Incorrect", "The sentence is not correct. Try again.", "OK"); } } private void OnCanValidateSentenceChanged(object sender, EventArgs e) { var validateButton = (Button)((StackLayout)Content).Children.Last(); validateButton.IsVisible = sentenceBuilder.SelectedWords.Any(); } } } ``` ### Explanation: - **AnimationDuration Property**: Set the `AnimationDuration` property to customize the duration of the button transition animation. This setup allows you to customize the animation duration for button transitions while maintaining default values. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/10/MAUISentenceBuilder8.gif?resize=640%2C348&ssl=1) ## Add Accessible feature to the component Handling accessibility features in your `SentenceBuilder` component is crucial to ensure that it is usable by everyone, including those with disabilities. Here are some key steps and considerations to make your component more accessible: ### 1. **Semantic Elements** Use semantic elements and properties to ensure that assistive technologies can interpret your UI correctly. - **Labels and Descriptions**: Use the `AutomationProperties` class to set labels and descriptions for buttons and other interactive elements. - **Content Descriptions**: Provide meaningful content descriptions for non-text elements like images or icons. ### 2. **Keyboard Navigation** Ensure that all interactive elements can be navigated using the keyboard. - **Tab Order**: Set the `TabIndex` property to control the order in which elements receive focus. - **Focus Visuals**: Ensure that focused elements are visually distinguishable. ### 3. **Accessible Names and Roles** Assign accessible names and roles to UI elements. - **AutomationProperties.Name**: Set this property to provide a name for the element that can be read by screen readers. - **AutomationProperties.HelpText**: Use this property to provide additional context or instructions. ### 4. **Touch Target Size** Ensure that touch targets are large enough to be easily tapped. - **Minimum Size**: Follow guidelines for minimum touch target sizes (e.g., 48×48 dp). ### 5. **Color Contrast** Ensure sufficient color contrast between text and background. - **Contrast Ratios**: Use tools to check that your color contrast meets accessibility standards (e.g., WCAG 2.1). ### 6. **Dynamic Updates** Notify assistive technologies of dynamic content changes. - **Live Regions**: Use `AutomationProperties.LiveSetting` to inform screen readers about updates to dynamic content. ### 7. **Testing** Regularly test your component with accessibility tools. - **Screen Readers**: Test with screen readers like NVDA, JAWS, or VoiceOver. - **Accessibility Insights**: Use tools like Accessibility Insights for automated testing. ### Example Implementation Here’s an example of how you can implement some of these features in your `SentenceBuilder` component. ``` private void UpdateWordButtons() { availableWordsLayout.Children.Clear(); selectedWordsLayout.Children.Clear(); foreach (var word in AvailableWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnAvailableWordClicked; button.SetValue(SemanticProperties.DescriptionProperty, word); button.SetValue(SemanticProperties.HintProperty, $"Button for {word}"); availableWordsLayout.Children.Add(button); if (!placeholders.ContainsKey(word)) { var placeholder = new BoxView { Color = PlaceholderColor, WidthRequest = 80, HeightRequest = 40, HorizontalOptions = LayoutOptions.Center, VerticalOptions = LayoutOptions.Center, IsVisible = false }; placeholders[word] = placeholder; } availableWordsLayout.Children.Add(placeholders[word]); } foreach (var word in selectedWords) { var button = new Button { Text = word, FontSize = TextSize, FontFamily = FontFamily, BackgroundColor = ButtonColor }; button.Clicked += OnSelectedWordClicked; button.SetValue(SemanticProperties.DescriptionProperty, word); button.SetValue(SemanticProperties.HintProperty, $"Selected button for {word}"); selectedWordsLayout.Children.Add(button); } validateButton.IsVisible = selectedWords.Any(); CanValidateSentenceChanged?.Invoke(this, EventArgs.Empty); } ``` ### Explanation - **AutomationProperties**: Added `AutomationProperties.Name` and `AutomationProperties.HelpText` to buttons for better screen reader support. - **Focus Visuals**: Ensure that buttons have clear focus visuals. - **Touch Target Size**: Ensure buttons are large enough to be easily tapped. ## Wrap up I hope this code can help you and give you an example of an MAUI component. Please keep in contact if you have any questions or improvements. **Categories:** .NET8, MAUI **Tags:** maui, net8 --- ### [Maui error Javax.Crypto.AEADBadTagException](https://puresourcecode.com/dotnet/maui/maui-error-javax-crypto-aeadbadtagexception/) **Published:** September 30, 2024 **Author:** Enrico **Excerpt:** What is the workaround when you get the error Maui error Javax.Crypto.AEADBadTagException with MAUI version 8.0.82 **Content:** Here we are again. After upgrading MAUI to version 8.0.82, when I deploy my application on an Android device, I get this error ``` Maui error Javax.Crypto.AEADBadTagException ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-8.png?resize=640%2C270&ssl=1)If you run the application in an Android Emulator, the application is working normally and you can’t get this issue. ## What can it be? From what I read on the internet, the issue is related to the `SecureStorage`. This is if you use the MAUI Essentials. The issue could have come from this component based on the [latest changes](https://github.com/dotnet/maui/pull/4211/files#diff-93ca7559b0a2eaf2bab3285f6d90c92ed92eee598bfbc814b368201753a0ef29). Check if under the **Android** platform under the `XML` folder, you have this `auto_backup_rules.xml` ``` ``` On GitHub, I read that the workaround is to set to `False` in the `AndroidManifest` the backup like that ``` ``` but I continue to receive the message and I can’t run the application. Checking the [preview documentation](https://github.com/dotnet/docs-maui/blob/3bc393baacecec73e806fda94f320a13e85ba436/docs/platform-integration/storage/secure-storage.md), I read I have to update also the *auto\_backup\_rules.xml* like the following ``` ``` Also, this is not working. ## Workaround After a lot of trials, setting `android:allowBackup="false"` was enough to fix the issue. It is not necessary to do anything else. The important thing is you have to remove the app from your device before deploying the application again. If you deploy the application and it exists on the phone, probably the app won’t work. Conclusion Please let me know if this is working for you or if you have any other workaround. **Categories:** .NET8, MAUI **Tags:** android, maui, secure-storage --- ### [Sequoia doesn't work with MAUI](https://puresourcecode.com/tools/macos/sequoia-doesnt-work-with-maui/) **Published:** September 19, 2024 **Author:** Enrico **Excerpt:** Sequoia doesn't work with MAUI. Now I can't try my apps on iOS. It is still working for macOS. Here the solution! **Content:** I have just updated my iMac to Sequoia. I also updated Xcode to the new version containing iOS 18. However, Sequoia doesn’t work with MAUI. Now I can’t try my apps on iOS. It is still working for macOS. After this update, neither Visual Studio for Mac nor Visual Studio on Windows can see the list of iOS simulators. So, now, I can’t test my [app](https://puresourcecode.com/news/language-in-use-is-here/). ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-6.png?resize=640%2C235&ssl=1) There is no update for Visual Studio; Visual Studio Code is not working for MAUI as usual. With Visual Studio for Mac I can run the application for macOS and Android but not for iOS (and the *Generic Simulator* doesn’t do anything. From Visual Studio, when I try to connect my iMac, I get a different error. > Object reference not set to an instance of an object. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-7.png?resize=590%2C545&ssl=1) I opened *Xcode* and agreed on all the licenses. It is getting difficult and difficult to work with MAUI… ## Visual Studio update Today, Microsoft released the Visual Studio 17.11.4. This version can connect to the iMac but I can’t see the list of simulators for iOS. ## Check this solution After a long search, I found this workaround by looking at the GitHub page for [Xamarin](https://github.com/xamarin/xamarin-macios/issues/20802). So, log on to the [Apple Developer Account](https://developer.apple.com/account) and scroll down to this point. By the way, all the links are to the Apple Developer portal on the right page. I describe the journey for reference. ![Apple Developer Portal - Sequoia doesn't work with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Get-Apple-Tools.png?resize=640%2C475&ssl=1)Apple Developer Portal Now, click on **Get the latest tools and beta SDKs** and you should see this page. In the section after the title, you find a link to [Install Apple Beta Software](https://developer.apple.com/support/install-beta/). ![- Sequoia doesn't work with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Apple-Operating-System.png?resize=640%2C488&ssl=1) Clicking on this link, you go to the page “[Installing and using Apple beta software](https://developer.apple.com/support/install-beta/)“. You see a menu on the left side. From this menu, click on **Xcode beta**. ![Installing and using Apple beta software - Sequoia doesn't work with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Installing-and-using-Apple-beta-software.png?resize=640%2C465&ssl=1)Installing and using Apple beta software Then, you jump to the section **Xcode beta** and then click on the link [View Xcode betas](https://developer.apple.com/download/all/). ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/View-Xcode-beta.png?resize=640%2C233&ssl=1)View Xcode beta Now, you see the page called [More Download](https://developer.apple.com/download/all/) and here there are a lot of tools and downloads. For the purpose of running an [MAUI](https://puresourcecode.com/?s=maui) application, I’m looking for a particular one. ![More downloads](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Apple-More-Download.png?resize=640%2C599&ssl=1)More downloads What we are looking for is **Xcode beta version 16.0 from June** (Simulator 16.0 – 1033) and the corresponding **Command Line Tool for Xcode 16 beta** (16A5171c). For your convenience, here are the links: - [Xcode 16 beta.xip](https://download.developer.apple.com/Developer_Tools/Xcode_16_beta/Xcode_16_beta.xip) - [Command Line Tools for Xcode 16 beta.dmg](https://download.developer.apple.com/Developer_Tools/Command_Line_Tools_for_Xcode_16_beta/Command_Line_Tools_for_Xcode_16_beta.dmg) ![- Sequoia doesn't work with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Screenshot-2024-09-19-at-11.50.17.png?resize=640%2C433&ssl=1) For your information, I tried the newest versions of Xcode but they don’t seem working. At the time I’m writing this post, the latest version of **Xcode is 16.1 beta 2** but it is not working with MAUI. ## Wrap up I hope this post can help you with your MacOS update to Sequoia because it doesn’t work with MAUI and Visual Studio for Windows or Mac (although is dismissed). Please let me know if you have any questions or I can help you. **Categories:** macOS, MAUI **Tags:** macOS, macOS-sequoia, maui **Hashtags:** macOS, maui --- ### [Picker doesn't work for MacCatalyst](https://puresourcecode.com/dotnet/net8/picker-doesnt-work-for-maccatalyst/) **Published:** September 16, 2024 **Author:** Enrico **Excerpt:** The MAUI Picker doesn't work for MacCatalyst. And this is quite annoying. Here an idea how implement a workaround to this issue. **Content:** If you are creating a project with MAUI and try your application on different platforms, you will soon discover this issue. The `Picker` doesn’t work for MacCatalyst. And this is quite annoying. ## Scenario So, on your page you added a Picker like the following ``` ``` After searching on the internet, I found that a few developers are complaining about this issue. For example, on [GitHub](https://github.com/dotnet/maui/issues/18015). or [Stackoverflow](https://stackoverflow.com/questions/76279366/maui-the-picker-doesnt-show-items). The Maui Picker is a wrapper around a `UITextField` that fires a `UIAlert` to show a picker. So, from what I read, I believe the way this worked was by using `UITextView`. When you tapped it, it will fire an event on edit. This event creates a `UIAlertController` with an `ActionSheet` style. It then hijacks the alert by replacing its underlying view with a `UIPickerView`. Then, a possible solution is to remove the `Title` from the `Picker` if you added one for the `Picker`. A few people said that the `Title` caused this problem during the render. So, you can add some conditional around the title like that ``` ``` In my case, this wasn’t the case and so I still have the issue. ## My workaround This is not very elegant. It allows me to avoid rethinking the UI for my apps. I use the `Picker` for all platforms apart from MacCatalyst. So, this is my original code to display a `SearchBar` and a `Picker` to filter a `ListView`. ``` ``` ![SearchBar and Picker for iOS - Picker doesn't work for MacCatalyst](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Screenshot-2024-09-16-at-23.51.12.png?resize=640%2C151&ssl=1)SearchBar and Picker for iOS This is not working for MacCatalyst. So, I hide the `Picker` when the application runs on it. In one of my previous post, I implemented a [tabpage with `RadioButton`](https://puresourcecode.com/dotnet/maui/create-tabbar-in-maui) and I’m going to use part of it. The XAML is now like that ``` ``` Now, the magic happens in the `IsVisible` condition. Using `OnPlatform`, I show another component for MacCatalyst. ### Implementation explains Now, I use the same source called `Filters` that is an `ObservableCollection` of `FilterModel` that is my model that has `Name` and `Value`. With the `HorizontalStackLayout`, I use the `BindableLayout` to display the `RadioButton`. What you see when the application runs in MacCatalyst is in the following screenshot ![RadioButton replaces Picker](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Screenshot-2024-09-17-at-00.01.15.png?resize=640%2C66&ssl=1)RadioButton replaces Picker So, the user can see this implementation based on the platform. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/Screenshot-2024-09-17-at-00.08.41.png?resize=640%2C111&ssl=1) **Categories:** .NET8 **Tags:** macCatalyst, macOS, maui, net8 **Hashtags:** macCatalyst, macOS --- ### [Language Dropdown for MAUI](https://puresourcecode.com/dotnet/maui/language-dropdown-for-maui/) **Published:** September 10, 2024 **Author:** Enrico **Excerpt:** I released a new component called Language Dropdown for MAUI. This displays a dropdown with all the languages in the world with their flags **Content:** Today, I released a new component called Language Dropdown for [MAUI](https://puresourcecode.com/tag/maui/) ([NET8](https://puresourcecode.com/tag/net8/)). This is a beautiful new component. It allows you to display a dropdown with all the languages in the world with their flags. If you are interested in languages in your application, look my other repositories and posts: - [CSharpCountryData: A simple cross platform offline .NET library for getting country data](https://github.com/erossini/CSharpCountryData) - [CSharp Country Data](https://puresourcecode.com/dotnet/net8/country-data-library-for-net8/) - [Demo Source Code](https://github.com/erossini/MAUILanguageDropdown) - [NuGet package](https://www.nuget.org/packages/PSC.Maui.Components.LanguageDropdown/) The Language Dropdown for MAUI is available on [NuGet](https://www.nuget.org/packages/PSC.Maui.Components.LanguageDropdown/) as a package to use in your non-commercial applications. If you would like to use it for commercial use, please send me a message. ## CultureInfo in NET8 If you use NET8 and want to retrieve the data related to the `CutureInfo`, you get a very large and interesting details such as the native name, the name of the language in English, calendar and numeric format, and a lot of other information. Generally speaking, we can say, CultureInfo provides information about a specific culture (called a locale for unmanaged code development). The information includes the names for the culture, the writing system, the calendar used, the sort order of strings, and formatting for dates and numbers. We know NET8 is cross-platform and for this reason we can create cross-platform applications using MAUI. Unfortunately, there is an issue when an application runs in iOS or macOS. ### Wrong DisplayName and EnglishName in CultureInfo on iOS When you use the `CultureInfo` in iOS or macOS, the information are not set properly. Following a thread on [GitHub](https://github.com/dotnet/maui/issues/14148), this is an issue related to the runtime. ![CultureInfo in iOS doesn't have all the corrent values](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image.png?resize=640%2C477&ssl=1)CultureInfo in iOS This is, 100%, a runtime issue, [dotnet/runtime#70028](https://github.com/dotnet/runtime/issues/70028) It works for you in Xamarin.Forms because that’s running on Mono. The code in the newer .NET runtimes is, apparently, linking out some of this data used for region info, and stripped out the names. This has no relationship to the MAUI UI project. If you built a .NET iOS or Catalyst app without MAUI UI code, the same thing would happen (as I’m doing right now, hence why I saw it). If you enable Hybrid Globalization, the `NativeName`, `EnglishName`, and `DisplayName`, should appear in the culture info. This only applies to .NET 8, and affects any .NET 8 app running on iOS, Catalyst, and tvOS. If you have issues with it, it should go to Runtime. So, this is quite annoying because I’m trying to use the `CultureInfo` to display a list of languages in a dropdown list. Now, I have to find a solution. ## The idea behind the component As you may know, I’m creating an app called [Language In Use](https://languageinuse.com/) to help learning a new language: this app offers tools to create your own dictionaries and study what you want. I created a [post](https://puresourcecode.com/news/language-in-use-is-here/) to explain what my idea and goal is. Now, in this app, a user can create a dictionary in order to contain all the words wants to learn. Each dictionary has the native language and the foreign language. How many languages do we have in the words? So, what I like in the UI is that the user can easily select a language from a list. In the list, the user can see the flag and the name of the language. This must be a simple dropdown like a normal `Picker` in MAUI. For example, this could be an idea how I want the dropdown. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-2.png?resize=324%2C406&ssl=1) ### Requirements Now, if have to create a wireframe of the result I like. Obviously, I want to see the list of the languages. For each language, I like to see the flag of the country where the language is spoken to easily find the correct one. ![List of countries](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-3.png?resize=422%2C750&ssl=1)List of countries Then, the list of countries must be filterable with a simple search. For the sake of the application, I like to have a simple `Picker` – or dropdown as I call it here – to filter. ![Filter the countries](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-4.png?resize=422%2C750&ssl=1)Filter the countries For example, I want to show only a specific list of supported languages or the recent languages I selected or are my favorite. ![Favorite/Support languages](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/09/image-5.png?resize=422%2C750&ssl=1)Favorite/Support languages Then, when the user clicks on a language, this must be displayed in the original `Picker` and the values must be available to the application. Also, the component must be used via MVVM. As a **Should have** feature, the flags has be in SVG format and available to use in other applications without necessary using the component. Generally speaking, in a country, people speak different languages, maybe using a dialact, and so I like to display the diversity. ## Implementation Now, the implementation is quite complex although is based on `CultureInfo`. The process to aggregate the languages is quite tricky. Finally, I sorted out all the issues and the component is working quite well. Bacause I spent a lot of time to create it, I published the component as a NuGet package and you can use it in non-commercial application. For this reason and for now, I won’t publish the source code of the component. If you want to use it for commercial use, please send me a message. ### Flags The flags for all countries and languages are present in the component. If you add the Language Dropdown for MAUI in your applications, automatically, you can display all the the flags in your application. All the flags are in SVG format. In order to display a flag in your project, it is enough to add an `Image` component and, as usual, selected as `Source` of the `Image` with the `.png` extension. For example: ``` ``` If you want to know what flags are available and related to what language, see the table below. ## Usage First, the package has to be added to your project. You can install it with this command from the dotnet CLI ``` dotnet add package PSC.Maui.Components.LanguageDropdown --version 8.0.0 ``` or with the NuGet command ``` Install-Package PSC.Maui.Components.LanguageDropdown -Version 8.0.0 ``` Then, the component has to be registered in your application. The component requires the `CommunityToolkit.Maui` and must be added in the `MauiProgram.cs` after that. ``` public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() .UseMauiCommunityToolkit() .UseLanguageDropdown() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); #if DEBUG builder.Logging.AddDebug(); #endif return builder.Build(); } } ``` Now, you can use the component in the application. ### How to add the component In your `MAUI` `ContentView` or `ContentPage`. the reference to the component must be added like in the following example: ``` ``` The code above shows the following screenshot ![image](https://github.com/user-attachments/assets/a98d6611-d40f-4f78-9647-931f0b130e85)## Properties NameTypeDescriptionBorderColorColorGets or sets the color or the border of the componentIsDisplayPickerControlboolGets or sets if the list of languages is displayedPlaceholderstringGets or sets the text to display as a placeholder until no selection is madeSelectedItemLanguageModelGets or sets the selected value## Languages and flags Culture NameAbbreviationFlagParentAfaraaf\_djAfar (Djibouti)aa-DJf\_djaaAfar (Eritrea)aa-ERf\_eraaAfar (Ethiopia)aa-ETf\_etaaAfrikaansaff\_naAfrikaans (Namibia)af-NAf\_naafAfrikaans (South Africa)af-ZAf\_zaafAghemagqf\_cmAghem (Cameroon)agq-CMf\_cmagqAkanakf\_ghAkan (Ghana)ak-GHf\_ghakAlbaniansqf\_alAlbanian (Albania)sq-ALf\_alsqAlbanian (Kosovo)sq-XKf\_alsqAlbanian (North Macedonia)sq-MKf\_mksqAmharicamf\_etAmharic (Ethiopia)am-ETf\_etamArabicarf\_aeArabic (Algeria)ar-DZf\_dzarArabic (Bahrain)ar-BHf\_bharArabic (Chad)ar-TDf\_tdarArabic (Comoros)ar-KMf\_kmarArabic (Djibouti)ar-DJf\_djarArabic (Egypt)ar-EGf\_egarArabic (Eritrea)ar-ERf\_erarArabic (Iraq)ar-IQf\_iqarArabic (Israel)ar-ILf\_ilarArabic (Jordan)ar-JOf\_joarArabic (Kuwait)ar-KWf\_kwarArabic (Lebanon)ar-LBf\_lbarArabic (Libya)ar-LYf\_lyarArabic (Mauritania)ar-MRf\_mrarArabic (Morocco)ar-MAf\_maarArabic (Oman)ar-OMf\_omarArabic (Palestinian Territories)ar-PSf\_psarArabic (Qatar)ar-QAf\_qaarArabic (Saudi Arabia)ar-SAf\_saarArabic (Somalia)ar-SOf\_soarArabic (South Sudan)ar-SSf\_aearArabic (Sudan)ar-SDf\_sdarArabic (Syria)ar-SYf\_syarArabic (Tunisia)ar-TNf\_tnarArabic (United Arab Emirates)ar-AEf\_aearArabic (World)ar-001f\_aearArabic (Yemen)ar-YEf\_yearArmenianhyf\_amArmenian (Armenia)hy-AMf\_amhyAssameseasf\_inAssamese (India)as-INf\_inasAsturianastf\_esAsturian (Spain)ast-ESf\_esastAsuasaf\_tzAsu (Tanzania)asa-TZf\_tzasaAzerbaijaniazf\_azAzerbaijani (Cyrillic, Azerbaijan)az-Cyrl-AZf\_azaz-CyrlAzerbaijani (Cyrillic)az-Cyrlf\_azazAzerbaijani (Latin, Azerbaijan)az-Latn-AZf\_azaz-LatnAzerbaijani (Latin)az-Latnf\_azazBafiaksff\_cmBafia (Cameroon)ksf-CMf\_cmksfBambarabmf\_mlBambara (Mali)bm-MLf\_mlbmBanglabnf\_bdBangla (Bangladesh)bn-BDf\_bdbnBangla (India)bn-INf\_inbnBasaabasf\_cmBasaa (Cameroon)bas-CMf\_cmbasBashkirbaf\_ruBashkir (Russia)ba-RUf\_rubaBasqueeuf\_esBasque (Spain)eu-ESf\_eseuBelarusianbef\_byBelarusian (Belarus)be-BYf\_bybeBembabemf\_zmBemba (Zambia)bem-ZMf\_zmbemBenabezf\_tzBena (Tanzania)bez-TZf\_tzbezBinibinf\_ngBini (Nigeria)bin-NGf\_ngbinBlinbynf\_erBlin (Eritrea)byn-ERf\_erbynBodobrxf\_inBodo (India)brx-INf\_inbrxBosnianbsf\_baBosnian (Cyrillic, Bosnia & Herzegovina)bs-Cyrl-BAf\_babs-CyrlBosnian (Cyrillic)bs-Cyrlf\_babsBosnian (Latin, Bosnia & Herzegovina)bs-Latn-BAf\_babs-LatnBosnian (Latin)bs-Latnf\_babsBretonbrf\_frBreton (France)br-FRf\_frbrBulgarianbgf\_bgBulgarian (Bulgaria)bg-BGf\_bgbgBurmesemyf\_mmBurmese (Myanmar \[Burma\])my-MMf\_mmmyCatalancaf\_adCatalan (Andorra)ca-ADf\_adcaCatalan (France)ca-FRf\_frcaCatalan (Italy)ca-ITf\_itcaCatalan (Spain)ca-ESf\_escaCebuanocebf\_phCebuano (Philippines)ceb-PHf\_phcebCentral Atlas Tamazighttzmf\_maCentral Atlas Tamazight (Algeria)tzm-DZf\_dztzmCentral Atlas Tamazight (Arabic, Morocco)tzm-Arab-MAf\_matzm-ArabCentral Atlas Tamazight (Arabic)tzm-Arabf\_matzmCentral Atlas Tamazight (Morocco)tzm-MAf\_matzmCentral Atlas Tamazight (Tifinagh, Morocco)tzm-Tfng-MAf\_matzm-TfngCentral Atlas Tamazight (Tifinagh)tzm-Tfngf\_matzmCentral Kurdishckbf\_iqCentral Kurdish (Iran)ckb-IRf\_irckbCentral Kurdish (Iraq)ckb-IQf\_iqckbChakmaccpf\_bdChakma (Bangladesh)ccp-BDf\_bdccpChakma (India)ccp-INf\_inccpChechencef\_ruChechen (Russia)ce-RUf\_ruceCherokeechrf\_usCherokee (United States)chr-USf\_uschrChigacggf\_ugChiga (Uganda)cgg-UGf\_ugcggChinesezhf\_cnChinese (Simplified, China)zh-Hans-CNf\_cnzh-HansChinese (Simplified, Hong Kong SAR China)zh-Hans-HKf\_hkzh-HansChinese (Simplified, Macao SAR)zh-Hans-MOf\_mozh-HansChinese (Simplified, Singapore)zh-Hans-SGf\_sgzh-HansChinese (Simplified)zh-Hansf\_cnzhChinese (Traditional, Hong Kong SAR China)zh-Hant-HKf\_hkzh-HantChinese (Traditional, Macao SAR)zh-Hant-MOf\_mozh-HantChinese (Traditional, Taiwan)zh-Hant-TWf\_twzh-HantChinese (Traditional)zh-Hantf\_cnzhChurch Slaviccuf\_ruChurch Slavic (Russia)cu-RUf\_rucuCologniankshf\_deColognian (Germany)ksh-DEf\_dekshCornishkwf\_gbCornish (United Kingdom)kw-GBf\_gbkwCorsicancof\_frCorsican (France)co-FRf\_frcoCroatianhrf\_hrCroatian (Bosnia & Herzegovina)hr-BAf\_bahrCroatian (Croatia)hr-HRf\_hrhrCzechcsf\_czCzech (Czechia)cs-CZf\_czcsDanishdaf\_dkDanish (Denmark)da-DKf\_dkdaDanish (Greenland)da-GLf\_gldaDivehidvf\_mvDivehi (Maldives)dv-MVf\_mvdvDogridoif\_inDogri (India)doi-INf\_indoiDualaduaf\_cmDuala (Cameroon)dua-CMf\_cmduaDutchnlf\_nlDutch (Aruba)nl-AWf\_awnlDutch (Belgium)nl-BEf\_benlDutch (Caribbean Netherlands)nl-BQf\_nlnlDutch (Curaçao)nl-CWf\_nlnlDutch (Netherlands)nl-NLf\_nlnlDutch (Sint Maarten)nl-SXf\_nlnlDutch (Suriname)nl-SRf\_srnlDzongkhadzf\_btDzongkha (Bhutan)dz-BTf\_btdzEmbuebuf\_keEmbu (Kenya)ebu-KEf\_keebuEnglishenf\_usEnglish (American Samoa)en-ASf\_asenEnglish (Anguilla)en-AIf\_aienEnglish (Antigua & Barbuda)en-AGf\_agenEnglish (Australia)en-AUf\_auenEnglish (Austria)en-ATf\_atenEnglish (Bahamas)en-BSf\_bsenEnglish (Barbados)en-BBf\_bbenEnglish (Belgium)en-BEf\_beenEnglish (Belize)en-BZf\_bzenEnglish (Bermuda)en-BMf\_bmenEnglish (Botswana)en-BWf\_bwenEnglish (British Indian Ocean Territory)en-IOf\_ioenEnglish (British Virgin Islands)en-VGf\_vgenEnglish (Burundi)en-BIf\_bienEnglish (Cameroon)en-CMf\_cmenEnglish (Canada)en-CAf\_caenEnglish (Caribbean)en-029f\_usenEnglish (Cayman Islands)en-KYf\_kyenEnglish (Christmas Island)en-CXf\_cxenEnglish (Cocos \[Keeling\] Islands)en-CCf\_ccenEnglish (Cook Islands)en-CKf\_ckenEnglish (Cyprus)en-CYf\_cyenEnglish (Denmark)en-DKf\_dkenEnglish (Dominica)en-DMf\_dmenEnglish (Eritrea)en-ERf\_erenEnglish (Eswatini)en-SZf\_szenEnglish (Europe)en-150f\_usenEnglish (Falkland Islands)en-FKf\_fkenEnglish (Fiji)en-FJf\_fjenEnglish (Finland)en-FIf\_fienEnglish (Gambia)en-GMf\_gmenEnglish (Germany)en-DEf\_deenEnglish (Ghana)en-GHf\_ghenEnglish (Gibraltar)en-GIf\_gienEnglish (Grenada)en-GDf\_gdenEnglish (Guam)en-GUf\_guenEnglish (Guernsey)en-GGf\_usenEnglish (Guyana)en-GYf\_gyenEnglish (Hong Kong SAR China)en-HKf\_hkenEnglish (India)en-INf\_inenEnglish (Indonesia)en-IDf\_idenEnglish (Ireland)en-IEf\_ieenEnglish (Isle of Man)en-IMf\_usenEnglish (Israel)en-ILf\_ilenEnglish (Jamaica)en-JMf\_jmenEnglish (Jersey)en-JEf\_usenEnglish (Kenya)en-KEf\_keenEnglish (Kiribati)en-KIf\_kienEnglish (Lesotho)en-LSf\_lsenEnglish (Liberia)en-LRf\_lrenEnglish (Macao SAR)en-MOf\_moenEnglish (Madagascar)en-MGf\_mgenEnglish (Malawi)en-MWf\_mwenEnglish (Malaysia)en-MYf\_myenEnglish (Malta)en-MTf\_mtenEnglish (Marshall Islands)en-MHf\_mhenEnglish (Mauritius)en-MUf\_muenEnglish (Micronesia)en-FMf\_fmenEnglish (Montserrat)en-MSf\_msenEnglish (Namibia)en-NAf\_naenEnglish (Nauru)en-NRf\_nrenEnglish (Netherlands)en-NLf\_nlenEnglish (New Zealand)en-NZf\_nzenEnglish (Nigeria)en-NGf\_ngenEnglish (Niue)en-NUf\_nuenEnglish (Norfolk Island)en-NFf\_nfenEnglish (Northern Mariana Islands)en-MPf\_mpenEnglish (Pakistan)en-PKf\_pkenEnglish (Palau)en-PWf\_pwenEnglish (Papua New Guinea)en-PGf\_pgenEnglish (Philippines)en-PHf\_phenEnglish (Pitcairn Islands)en-PNf\_pnenEnglish (Puerto Rico)en-PRf\_prenEnglish (Rwanda)en-RWf\_rwenEnglish (Samoa)en-WSf\_wsenEnglish (Seychelles)en-SCf\_scenEnglish (Sierra Leone)en-SLf\_slenEnglish (Singapore)en-SGf\_sgenEnglish (Sint Maarten)en-SXf\_usenEnglish (Slovenia)en-SIf\_sienEnglish (Solomon Islands)en-SBf\_sbenEnglish (South Africa)en-ZAf\_zaenEnglish (South Sudan)en-SSf\_usenEnglish (St Helena)en-SHf\_shenEnglish (St Kitts & Nevis)en-KNf\_knenEnglish (St Lucia)en-LCf\_lcenEnglish (St Vincent & the Grenadines)en-VCf\_vcenEnglish (Sudan)en-SDf\_sdenEnglish (Sweden)en-SEf\_seenEnglish (Switzerland)en-CHf\_chenEnglish (Tanzania)en-TZf\_tzenEnglish (Tokelau)en-TKf\_tkenEnglish (Tonga)en-TOf\_toenEnglish (Trinidad & Tobago)en-TTf\_ttenEnglish (Turks & Caicos Islands)en-TCf\_tcenEnglish (Tuvalu)en-TVf\_tvenEnglish (Uganda)en-UGf\_ugenEnglish (United Arab Emirates)en-AEf\_aeenEnglish (United Kingdom)en-GBf\_gbenEnglish (United States, Computer)en-US-POSIXf\_usen-USEnglish (United States)en-USf\_usenEnglish (US Outlying Islands)en-UMf\_umenEnglish (US Virgin Islands)en-VIf\_vienEnglish (Vanuatu)en-VUf\_vuenEnglish (World)en-001f\_usenEnglish (Zambia)en-ZMf\_zmenEnglish (Zimbabwe)en-ZWf\_zwenEsperantoeof\_Esperanto (World)eo-001f\_eoEstonianetf\_eeEstonian (Estonia)et-EEf\_eeetEweeef\_ghEwe (Ghana)ee-GHf\_gheeEwe (Togo)ee-TGf\_tgeeEwondoewof\_cmEwondo (Cameroon)ewo-CMf\_cmewoFaroesefof\_foFaroese (Denmark)fo-DKf\_dkfoFaroese (Faroe Islands)fo-FOf\_fofoFilipinofilf\_phFilipino (Philippines)fil-PHf\_phfilFinnishfif\_fiFinnish (Finland)fi-FIf\_fifiFrenchfrf\_frFrench (Algeria)fr-DZf\_dzfrFrench (Belgium)fr-BEf\_befrFrench (Benin)fr-BJf\_bjfrFrench (Burkina Faso)fr-BFf\_bffrFrench (Burundi)fr-BIf\_bifrFrench (Cameroon)fr-CMf\_cmfrFrench (Canada)fr-CAf\_cafrFrench (Caribbean)fr-029f\_frfrFrench (Central African Republic)fr-CFf\_cffrFrench (Chad)fr-TDf\_tdfrFrench (Comoros)fr-KMf\_kmfrFrench (Congo – Brazzaville)fr-CGf\_cgfrFrench (Congo – Kinshasa)fr-CDf\_cdfrFrench (Côte d’Ivoire)fr-CIf\_cifrFrench (Djibouti)fr-DJf\_djfrFrench (Equatorial Guinea)fr-GQf\_gqfrFrench (France)fr-FRf\_frfrFrench (French Guiana)fr-GFf\_gffrFrench (French Polynesia)fr-PFf\_pffrFrench (Gabon)fr-GAf\_gafrFrench (Guadeloupe)fr-GPf\_gpfrFrench (Guinea)fr-GNf\_gnfrFrench (Haiti)fr-HTf\_htfrFrench (Luxembourg)fr-LUf\_lufrFrench (Madagascar)fr-MGf\_mgfrFrench (Mali)fr-MLf\_mlfrFrench (Martinique)fr-MQf\_mqfrFrench (Mauritania)fr-MRf\_mrfrFrench (Mauritius)fr-MUf\_mufrFrench (Mayotte)fr-YTf\_ytfrFrench (Monaco)fr-MCf\_mcfrFrench (Morocco)fr-MAf\_mafrFrench (New Caledonia)fr-NCf\_ncfrFrench (Niger)fr-NEf\_nefrFrench (Réunion)fr-REf\_refrFrench (Rwanda)fr-RWf\_rwfrFrench (Senegal)fr-SNf\_snfrFrench (Seychelles)fr-SCf\_scfrFrench (St Barthélemy)fr-BLf\_frfrFrench (St Martin)fr-MFf\_frfrFrench (St Pierre & Miquelon)fr-PMf\_pmfrFrench (Switzerland)fr-CHf\_chfrFrench (Syria)fr-SYf\_syfrFrench (Togo)fr-TGf\_tgfrFrench (Tunisia)fr-TNf\_tnfrFrench (Vanuatu)fr-VUf\_vufrFrench (Wallis & Futuna)fr-WFf\_wffrFriulianfurf\_itFriulian (Italy)fur-ITf\_itfurFulahfff\_bfFulah (Adlam, Burkina Faso)ff-Adlm-BFf\_bfff-AdlmFulah (Adlam, Cameroon)ff-Adlm-CMf\_cmff-AdlmFulah (Adlam, Gambia)ff-Adlm-GMf\_gmff-AdlmFulah (Adlam, Ghana)ff-Adlm-GHf\_ghff-AdlmFulah (Adlam, Guinea-Bissau)ff-Adlm-GWf\_gwff-AdlmFulah (Adlam, Guinea)ff-Adlm-GNf\_gnff-AdlmFulah (Adlam, Liberia)ff-Adlm-LRf\_lrff-AdlmFulah (Adlam, Mauritania)ff-Adlm-MRf\_mrff-AdlmFulah (Adlam, Niger)ff-Adlm-NEf\_neff-AdlmFulah (Adlam, Nigeria)ff-Adlm-NGf\_ngff-AdlmFulah (Adlam, Senegal)ff-Adlm-SNf\_snff-AdlmFulah (Adlam, Sierra Leone)ff-Adlm-SLf\_slff-AdlmFulah (Adlam)ff-Adlmf\_bfffFulah (Latin, Burkina Faso)ff-Latn-BFf\_bfff-LatnFulah (Latin, Cameroon)ff-Latn-CMf\_cmff-LatnFulah (Latin, Gambia)ff-Latn-GMf\_gmff-LatnFulah (Latin, Ghana)ff-Latn-GHf\_ghff-LatnFulah (Latin, Guinea-Bissau)ff-Latn-GWf\_gwff-LatnFulah (Latin, Guinea)ff-Latn-GNf\_gnff-LatnFulah (Latin, Liberia)ff-Latn-LRf\_lrff-LatnFulah (Latin, Mauritania)ff-Latn-MRf\_mrff-LatnFulah (Latin, Niger)ff-Latn-NEf\_neff-LatnFulah (Latin, Nigeria)ff-Latn-NGf\_ngff-LatnFulah (Latin, Senegal)ff-Latn-SNf\_snff-LatnFulah (Latin, Sierra Leone)ff-Latn-SLf\_slff-LatnFulah (Latin)ff-Latnf\_bfffGalicianglf\_esGalician (Spain)gl-ESf\_esglGandalgf\_ugGanda (Uganda)lg-UGf\_uglgGeorgiankaf\_geGeorgian (Georgia)ka-GEf\_gekaGermandef\_deGerman (Austria)de-ATf\_atdeGerman (Belgium)de-BEf\_bedeGerman (Germany)de-DEf\_dedeGerman (Italy)de-ITf\_itdeGerman (Liechtenstein)de-LIf\_lideGerman (Luxembourg)de-LUf\_ludeGerman (Switzerland)de-CHf\_chdeGreekelf\_cyGreek (Cyprus)el-CYf\_cyelGreek (Greece)el-GRf\_grelGuaranignf\_pyGuarani (Paraguay)gn-PYf\_pygnGujaratiguf\_inGujarati (India)gu-INf\_inguGusiiguzf\_keGusii (Kenya)guz-KEf\_keguzHausahaf\_ghHausa (Ghana)ha-GHf\_ghhaHausa (Niger)ha-NEf\_nehaHausa (Nigeria)ha-NGf\_nghaHawaiianhawf\_usHawaiian (United States)haw-USf\_ushawHebrewhef\_ilHebrew (Israel)he-ILf\_ilheHindihif\_inHindi (India)hi-INf\_inhiHungarianhuf\_huHungarian (Hungary)hu-HUf\_huhuIbibioibbf\_ngIbibio (Nigeria)ibb-NGf\_ngibbIcelandicisf\_isIcelandic (Iceland)is-ISf\_isisIgboigf\_ngIgbo (Nigeria)ig-NGf\_ngigInari Samismnf\_fiInari Sami (Finland)smn-FIf\_fismnIndonesianidf\_idIndonesian (Indonesia)id-IDf\_ididInterlinguaiaf\_Interlingua (World)ia-001f\_iaInuktitutiuf\_caInuktitut (Canada)iu-CAf\_caiuInuktitut (Latin, Canada)iu-Latn-CAf\_caiu-LatnInuktitut (Latin)iu-Latnf\_caiuIrishgaf\_gbIrish (Ireland)ga-IEf\_iegaIrish (United Kingdom)ga-GBf\_gbgaItalianitf\_itItalian (Italy)it-ITf\_ititItalian (San Marino)it-SMf\_smitItalian (Switzerland)it-CHf\_chitItalian (Vatican City)it-VAf\_vaitJapanesejaf\_jpJapanese (Japan)ja-JPf\_jpjaJavanesejvf\_idJavanese (Indonesia)jv-IDf\_idjvJavanese (Javanese, Indonesia)jv-Java-IDf\_idjv-JavaJavanese (Javanese)jv-Javaf\_idjvJola-Fonyidyof\_snJola-Fonyi (Senegal)dyo-SNf\_sndyoKabuverdianukeaf\_cvKabuverdianu (Cape Verde)kea-CVf\_cvkeaKabylekabf\_dzKabyle (Algeria)kab-DZf\_dzkabKakokkjf\_cmKako (Cameroon)kkj-CMf\_cmkkjKalaallisutklf\_glKalaallisut (Greenland)kl-GLf\_glklKalenjinklnf\_keKalenjin (Kenya)kln-KEf\_keklnKambakamf\_keKamba (Kenya)kam-KEf\_kekamKannadaknf\_inKannada (India)kn-INf\_inknKanurikrf\_ngKanuri (Latin, Nigeria)kr-Latn-NGf\_ngkr-LatnKanuri (Latin)kr-Latnf\_ngkrKashmiriksf\_inKashmiri (Arabic, India)ks-Arab-INf\_inks-ArabKashmiri (Arabic)ks-Arabf\_inksKashmiri (Devanagari, India)ks-Deva-INf\_inks-DevaKashmiri (Devanagari)ks-Devaf\_inksKazakhkkf\_kzKazakh (Kazakhstan)kk-KZf\_kzkkKhmerkmf\_khKhmer (Cambodia)km-KHf\_khkmKikuyukif\_keKikuyu (Kenya)ki-KEf\_kekiKinyarwandarwf\_rwKinyarwanda (Rwanda)rw-RWf\_rwrwKonkanikokf\_inKonkani (India)kok-INf\_inkokKoreankof\_kpKorean (North Korea)ko-KPf\_kpkoKorean (South Korea)ko-KRf\_krkoKoyra Chiinikhqf\_mlKoyra Chiini (Mali)khq-MLf\_mlkhqKoyraboro Sennisesf\_mlKoyraboro Senni (Mali)ses-MLf\_mlsesKwasionmgf\_cmKwasio (Cameroon)nmg-CMf\_cmnmgKyrgyzkyf\_kgKyrgyz (Kyrgyzstan)ky-KGf\_kgkyKʼicheʼqucf\_gtKʼicheʼ (Guatemala)quc-GTf\_gtqucLakotalktf\_usLakota (United States)lkt-USf\_uslktLangilagf\_tzLangi (Tanzania)lag-TZf\_tzlagLaolof\_laLao (Laos)lo-LAf\_laloLatinlaf\_vaLatin (Vatican City)la-VAf\_valaLatvianlvf\_lvLatvian (Latvia)lv-LVf\_lvlvLingalalnf\_aoLingala (Angola)ln-AOf\_aolnLingala (Central African Republic)ln-CFf\_cflnLingala (Congo – Brazzaville)ln-CGf\_cglnLingala (Congo – Kinshasa)ln-CDf\_cdlnLithuanianltf\_ltLithuanian (Lithuania)lt-LTf\_ltltLow Germanndsf\_nlLow German (Germany)nds-DEf\_dendsLow German (Netherlands)nds-NLf\_nlndsLower Sorbiandsbf\_deLower Sorbian (Germany)dsb-DEf\_dedsbLuba-Katangaluf\_cdLuba-Katanga (Congo – Kinshasa)lu-CDf\_cdluLule Samismjf\_seLule Sami (Norway)smj-NOf\_nosmjLule Sami (Sweden)smj-SEf\_sesmjLuoluof\_keLuo (Kenya)luo-KEf\_keluoLuxembourgishlbf\_luLuxembourgish (Luxembourg)lb-LUf\_lulbLuyialuyf\_keLuyia (Kenya)luy-KEf\_keluyMacedonianmkf\_mkMacedonian (North Macedonia)mk-MKf\_mkmkMachamejmcf\_tzMachame (Tanzania)jmc-TZf\_tzjmcMaithilimaif\_inMaithili (India)mai-INf\_inmaiMakhuwa-Meettomghf\_mzMakhuwa-Meetto (Mozambique)mgh-MZf\_mzmghMakondekdef\_tzMakonde (Tanzania)kde-TZf\_tzkdeMalagasymgf\_mgMalagasy (Madagascar)mg-MGf\_mgmgMalaymsf\_myMalay (Brunei)ms-BNf\_bnmsMalay (Indonesia)ms-IDf\_idmsMalay (Malaysia)ms-MYf\_mymsMalay (Singapore)ms-SGf\_sgmsMalayalammlf\_inMalayalam (India)ml-INf\_inmlMaltesemtf\_mtMaltese (Malta)mt-MTf\_mtmtManipurimnif\_inManipuri (Bangla, India)mni-Beng-INf\_inmni-BengManipuri (Bangla)mni-Bengf\_inmniManxgvf\_Manx (Isle of Man)gv-IMf\_gvMaorimif\_nzMaori (New Zealand)mi-NZf\_nzmiMapuchearnf\_clMapuche (Chile)arn-CLf\_clarnMarathimrf\_inMarathi (India)mr-INf\_inmrMasaimasf\_keMasai (Kenya)mas-KEf\_kemasMasai (Tanzania)mas-TZf\_tzmasMazanderanimznf\_irMazanderani (Iran)mzn-IRf\_irmznMerumerf\_keMeru (Kenya)mer-KEf\_kemerMetaʼmgof\_cmMetaʼ (Cameroon)mgo-CMf\_cmmgoMohawkmohf\_caMohawk (Canada)moh-CAf\_camohMongolianmnf\_mnMongolian (Mongolia)mn-MNf\_mnmnMongolian (Mongolian, China)mn-Mong-CNf\_cnmn-MongMongolian (Mongolian, Mongolia)mn-Mong-MNf\_mnmn-MongMongolian (Mongolian)mn-Mongf\_mnmnMorisyenmfef\_muMorisyen (Mauritius)mfe-MUf\_mumfeMundangmuaf\_cmMundang (Cameroon)mua-CMf\_cmmuaN’Konqof\_gnN’Ko (Guinea)nqo-GNf\_gnnqoNamanaqf\_naNama (Namibia)naq-NAf\_nanaqNepalinef\_npNepali (India)ne-INf\_inneNepali (Nepal)ne-NPf\_npneNgiemboonnnhf\_cmNgiemboon (Cameroon)nnh-CMf\_cmnnhNgombajgof\_cmNgomba (Cameroon)jgo-CMf\_cmjgoNigerian Pidginpcmf\_ngNigerian Pidgin (Nigeria)pcm-NGf\_ngpcmNorth Ndebelendf\_zwNorth Ndebele (Zimbabwe)nd-ZWf\_zwndNorthern Lurilrcf\_iqNorthern Luri (Iran)lrc-IRf\_irlrcNorthern Luri (Iraq)lrc-IQf\_iqlrcNorthern Samisef\_seNorthern Sami (Finland)se-FIf\_fiseNorthern Sami (Norway)se-NOf\_noseNorthern Sami (Sweden)se-SEf\_seseNorthern Sothonsof\_zaNorthern Sotho (South Africa)nso-ZAf\_zansoNorwegian Bokmålnbf\_noNorwegian Bokmål (Norway)nb-NOf\_nonbNorwegian Bokmål (Svalbard & Jan Mayen)nb-SJf\_sjnbNorwegian Nynorsknnf\_noNorwegian Nynorsk (Norway)nn-NOf\_nonnNuernusf\_Nuer (South Sudan)nus-SSf\_nusNyankolenynf\_ugNyankole (Uganda)nyn-UGf\_ugnynOccitanocf\_frOccitan (France)oc-FRf\_frocOdiaorf\_inOdia (India)or-INf\_inorOromoomf\_etOromo (Ethiopia)om-ETf\_etomOromo (Kenya)om-KEf\_keomOsseticosf\_geOssetic (Georgia)os-GEf\_geosOssetic (Russia)os-RUf\_ruosPapiamentopapf\_Papiamento (Caribbean)pap-029f\_papPashtopsf\_pkPashto (Afghanistan)ps-AFf\_afpsPashto (Pakistan)ps-PKf\_pkpsPersianfaf\_afPersian (Afghanistan)fa-AFf\_affaPersian (Iran)fa-IRf\_irfaPolishplf\_plPolish (Poland)pl-PLf\_plplPortugueseptf\_ptPortuguese (Angola)pt-AOf\_aoptPortuguese (Brazil)pt-BRf\_brptPortuguese (Cape Verde)pt-CVf\_cvptPortuguese (Equatorial Guinea)pt-GQf\_gqptPortuguese (Guinea-Bissau)pt-GWf\_gwptPortuguese (Luxembourg)pt-LUf\_luptPortuguese (Macao SAR)pt-MOf\_moptPortuguese (Mozambique)pt-MZf\_mzptPortuguese (Portugal)pt-PTf\_ptptPortuguese (São Tomé & Príncipe)pt-STf\_stptPortuguese (Switzerland)pt-CHf\_chptPortuguese (Timor-Leste)pt-TLf\_tlptPrussianprgf\_Prussian (World)prg-001f\_prgPunjabipaf\_pkPunjabi (Arabic, Pakistan)pa-Arab-PKf\_pkpa-ArabPunjabi (Arabic)pa-Arabf\_pkpaPunjabi (Gurmukhi, India)pa-Guru-INf\_inpa-GuruPunjabi (Gurmukhi)pa-Guruf\_inpaQuechuaquf\_boQuechua (Bolivia)qu-BOf\_boquQuechua (Ecuador)qu-ECf\_ecquQuechua (Peru)qu-PEf\_pequRomanianrof\_roRomanian (Moldova)ro-MDf\_mdroRomanian (Romania)ro-ROf\_roroRomanshrmf\_chRomansh (Switzerland)rm-CHf\_chrmRomboroff\_tzRombo (Tanzania)rof-TZf\_tzrofRundirnf\_biRundi (Burundi)rn-BIf\_birnRussianruf\_ruRussian (Belarus)ru-BYf\_byruRussian (Kazakhstan)ru-KZf\_kzruRussian (Kyrgyzstan)ru-KGf\_kgruRussian (Moldova)ru-MDf\_mdruRussian (Russia)ru-RUf\_ruruRussian (Ukraine)ru-UAf\_uaruRwarwkf\_tzRwa (Tanzania)rwk-TZf\_tzrwkSahossyf\_erSaho (Eritrea)ssy-ERf\_erssySamburusaqf\_keSamburu (Kenya)saq-KEf\_kesaqSangosgf\_cfSango (Central African Republic)sg-CFf\_cfsgSangusbpf\_tzSangu (Tanzania)sbp-TZf\_tzsbpSanskritsaf\_inSanskrit (India)sa-INf\_insaSantalisatf\_inSantali (Ol Chiki, India)sat-Olck-INf\_insat-OlckSantali (Ol Chiki)sat-Olckf\_insatScottish Gaelicgdf\_gbScottish Gaelic (United Kingdom)gd-GBf\_gbgdSenasehf\_mzSena (Mozambique)seh-MZf\_mzsehSerbiansrf\_baSerbian (Cyrillic, Bosnia & Herzegovina)sr-Cyrl-BAf\_basr-CyrlSerbian (Cyrillic, Kosovo)sr-Cyrl-XKf\_basr-CyrlSerbian (Cyrillic, Montenegro)sr-Cyrl-MEf\_mesr-CyrlSerbian (Cyrillic, Serbia)sr-Cyrl-RSf\_rssr-CyrlSerbian (Cyrillic)sr-Cyrlf\_basrSerbian (Latin, Bosnia & Herzegovina)sr-Latn-BAf\_basr-LatnSerbian (Latin, Kosovo)sr-Latn-XKf\_basr-LatnSerbian (Latin, Montenegro)sr-Latn-MEf\_mesr-LatnSerbian (Latin, Serbia)sr-Latn-RSf\_rssr-LatnSerbian (Latin)sr-Latnf\_basrShambalaksbf\_tzShambala (Tanzania)ksb-TZf\_tzksbShonasnf\_zwShona (Zimbabwe)sn-ZWf\_zwsnSichuan Yiiif\_cnSichuan Yi (China)ii-CNf\_cniiSindhisdf\_pkSindhi (Arabic, Pakistan)sd-Arab-PKf\_pksd-ArabSindhi (Arabic)sd-Arabf\_pksdSindhi (Devanagari, India)sd-Deva-INf\_insd-DevaSindhi (Devanagari)sd-Devaf\_insdSinhalasif\_lkSinhala (Sri Lanka)si-LKf\_lksiSkolt Samismsf\_fiSkolt Sami (Finland)sms-FIf\_fismsSlovakskf\_skSlovak (Slovakia)sk-SKf\_skskSlovenianslf\_siSlovenian (Slovenia)sl-SIf\_sislSogaxogf\_ugSoga (Uganda)xog-UGf\_ugxogSomalisof\_soSomali (Djibouti)so-DJf\_djsoSomali (Ethiopia)so-ETf\_etsoSomali (Kenya)so-KEf\_kesoSomali (Somalia)so-SOf\_sosoSouth Ndebelenrf\_zaSouth Ndebele (South Africa)nr-ZAf\_zanrSouthern Samismaf\_seSouthern Sami (Norway)sma-NOf\_nosmaSouthern Sami (Sweden)sma-SEf\_sesmaSouthern Sothostf\_lsSouthern Sotho (Lesotho)st-LSf\_lsstSouthern Sotho (South Africa)st-ZAf\_zastSpanishesf\_esSpanish (Argentina)es-ARf\_aresSpanish (Belize)es-BZf\_bzesSpanish (Bolivia)es-BOf\_boesSpanish (Brazil)es-BRf\_bresSpanish (Chile)es-CLf\_clesSpanish (Colombia)es-COf\_coesSpanish (Costa Rica)es-CRf\_cresSpanish (Cuba)es-CUf\_cuesSpanish (Dominican Republic)es-DOf\_doesSpanish (Ecuador)es-ECf\_ecesSpanish (El Salvador)es-SVf\_svesSpanish (Equatorial Guinea)es-GQf\_gqesSpanish (Guatemala)es-GTf\_gtesSpanish (Honduras)es-HNf\_hnesSpanish (Latin America)es-419f\_esesSpanish (Mexico)es-MXf\_mxesSpanish (Nicaragua)es-NIf\_niesSpanish (Panama)es-PAf\_paesSpanish (Paraguay)es-PYf\_pyesSpanish (Peru)es-PEf\_peesSpanish (Philippines)es-PHf\_phesSpanish (Puerto Rico)es-PRf\_presSpanish (Spain)es-ESf\_esesSpanish (United States)es-USf\_usesSpanish (Uruguay)es-UYf\_uyesSpanish (Venezuela)es-VEf\_veesStandard Moroccan Tamazightzghf\_maStandard Moroccan Tamazight (Morocco)zgh-MAf\_mazghSundanesesuf\_idSundanese (Latin, Indonesia)su-Latn-IDf\_idsu-LatnSundanese (Latin)su-Latnf\_idsuSwahiliswf\_cdSwahili (Congo – Kinshasa)sw-CDf\_cdswSwahili (Kenya)sw-KEf\_keswSwahili (Tanzania)sw-TZf\_tzswSwahili (Uganda)sw-UGf\_ugswSwatissf\_szSwati (Eswatini)ss-SZf\_szssSwati (South Africa)ss-ZAf\_zassSwedishsvf\_seSwedish (Åland Islands)sv-AXf\_axsvSwedish (Finland)sv-FIf\_fisvSwedish (Sweden)sv-SEf\_sesvSwiss Germangswf\_chSwiss German (France)gsw-FRf\_frgswSwiss German (Liechtenstein)gsw-LIf\_ligswSwiss German (Switzerland)gsw-CHf\_chgswSyriacsyrf\_sySyriac (Syria)syr-SYf\_sysyrTachelhitshif\_maTachelhit (Latin, Morocco)shi-Latn-MAf\_mashi-LatnTachelhit (Latin)shi-Latnf\_mashiTachelhit (Tifinagh, Morocco)shi-Tfng-MAf\_mashi-TfngTachelhit (Tifinagh)shi-Tfngf\_mashiTaitadavf\_keTaita (Kenya)dav-KEf\_kedavTajiktgf\_tjTajik (Tajikistan)tg-TJf\_tjtgTamiltaf\_inTamil (India)ta-INf\_intaTamil (Malaysia)ta-MYf\_mytaTamil (Singapore)ta-SGf\_sgtaTamil (Sri Lanka)ta-LKf\_lktaTasawaqtwqf\_neTasawaq (Niger)twq-NEf\_netwqTatarttf\_ruTatar (Russia)tt-RUf\_ruttTelugutef\_inTelugu (India)te-INf\_inteTesoteof\_keTeso (Kenya)teo-KEf\_keteoTeso (Uganda)teo-UGf\_ugteoThaithf\_thThai (Thailand)th-THf\_ththTibetanbof\_cnTibetan (China)bo-CNf\_cnboTibetan (India)bo-INf\_inboTigretigf\_erTigre (Eritrea)tig-ERf\_ertigTigrinyatif\_erTigrinya (Eritrea)ti-ERf\_ertiTigrinya (Ethiopia)ti-ETf\_ettiTongantof\_toTongan (Tonga)to-TOf\_totoTsongatsf\_zaTsonga (South Africa)ts-ZAf\_zatsTswanatnf\_bwTswana (Botswana)tn-BWf\_bwtnTswana (South Africa)tn-ZAf\_zatnTurkishtrf\_trTurkish (Cyprus)tr-CYf\_cytrTurkish (Turkey)tr-TRf\_trtrTurkmentkf\_tmTurkmen (Turkmenistan)tk-TMf\_tmtkUkrainianukf\_uaUkrainian (Ukraine)uk-UAf\_uaukUpper Sorbianhsbf\_deUpper Sorbian (Germany)hsb-DEf\_dehsbUrduurf\_inUrdu (India)ur-INf\_inurUrdu (Pakistan)ur-PKf\_pkurUyghurugf\_cnUyghur (China)ug-CNf\_cnugUzbekuzf\_uzUzbek (Arabic, Afghanistan)uz-Arab-AFf\_afuz-ArabUzbek (Arabic)uz-Arabf\_afuzUzbek (Cyrillic, Uzbekistan)uz-Cyrl-UZf\_uzuz-CyrlUzbek (Cyrillic)uz-Cyrlf\_uzuzUzbek (Latin, Uzbekistan)uz-Latn-UZf\_uzuz-LatnUzbek (Latin)uz-Latnf\_uzuzVaivaif\_lrVai (Latin, Liberia)vai-Latn-LRf\_lrvai-LatnVai (Latin)vai-Latnf\_lrvaiVai (Vai, Liberia)vai-Vaii-LRf\_lrvai-VaiiVai (Vai)vai-Vaiif\_lrvaiVendavef\_zaVenda (South Africa)ve-ZAf\_zaveVietnamesevif\_vnVietnamese (Vietnam)vi-VNf\_vnviVolapükvof\_Volapük (World)vo-001f\_voVunjovunf\_tzVunjo (Tanzania)vun-TZf\_tzvunWalserwaef\_chWalser (Switzerland)wae-CHf\_chwaeWelshcyf\_gbWelsh (United Kingdom)cy-GBf\_gbcyWestern Frisianfyf\_fyWestern Frisian (Netherlands)fy-NLf\_nlfyWolayttawalf\_etWolaytta (Ethiopia)wal-ETf\_etwalWolofwof\_snWolof (Senegal)wo-SNf\_snwoXhosaxhf\_zaXhosa (South Africa)xh-ZAf\_zaxhYakutsahf\_ruYakut (Russia)sah-RUf\_rusahYangbenyavf\_cmYangben (Cameroon)yav-CMf\_cmyavYiddishyif\_Yiddish (World)yi-001f\_yiYorubayof\_bjYoruba (Benin)yo-BJf\_bjyoYoruba (Nigeria)yo-NGf\_ngyoZarmadjef\_neZarma (Niger)dje-NEf\_nedjeZuluzuf\_zaZulu (South Africa)zu-ZAf\_zazu## Screenshot So, here you have some GIFs when I show the Language Dropdown component for MAUI in action. I really like the simple design and the colorful flags. ### iOS ![Screenshot for iOS - Language Dropdown for MAUI](https://github.com/user-attachments/assets/abcdc47a-0e65-4ea0-9b63-a10c4c1dc1b3)Language Dropdown for iOS in action ### Windows ![Screenshot for Windows - Language Dropdown for MAUI](https://github.com/user-attachments/assets/79ff833b-3c31-4eba-b8d1-6989fb6640c5)Language Dropdown for Windows in action ### Android ![Screenshot for Android- Language Dropdown for MAUI](https://github.com/user-attachments/assets/c923ba2c-7405-491d-a78a-a23e6768d713)Language Dropdown for Android in action **Categories:** .NET8, MAUI **Tags:** maui **Hashtags:** maui --- ### [First look at Star Wars Outlaws](https://puresourcecode.com/games/first-look-at-star-wars-outlaws/) **Published:** August 27, 2024 **Author:** Enrico **Excerpt:** Here is the first look at Star Wars Outlaws, the most awaited game of the year. The Star Wars universe is expanding once again. **Content:** Here is the first look at Star Wars Outlaws, the most awaited game of the year after rumours about other Star Wars games like [Eclipse](https://www.puresourcecode.com/games/is-star-wars-eclipse-coming) or [Squadron](https://www.puresourcecode.com/games/star-wars-squadrons-is-an-intricate-dogfighter). The Star Wars universe is expanding once again. This time it’s bringing fans an experience like never before with the upcoming release of Star Wars Outlaws. Set to launch on August 30, 2024, this game promises to be a groundbreaking addition to the Star Wars franchise. It offers players the chance to explore the galaxy in an open-world format. If, like me, you pre-ordered the game, at midnight last night (August 27th 2024) it was available. ## My first 2 hours First thing this morning: play with the game! Playing with this game is quite enjoyable. I can go around the city and the design is very nice. I found tricky to understand how to unlock the locks and I spent a lot of time on that. Also, I’m not very good at shooting games and the first shouting it was a pain. Overall, I can tell the game remain me a lot of [Hogwarts Legacy](https://www.puresourcecode.com/games/hogwarts-legacy-is-a-successful-game) like pets to cuddle and lock to open. ### Lock tips So, I have a quick tip about the locks. When you have to use the data spike ## What is Star Wars Outlaws? Star Wars Outlaws was developed by Massive Entertainment and published by [Ubisoft](https://puresourcecode.com/?hashtags=game). It is poised to fill the gap between the events of The Empire Strikes Back and Return of the Jedi. Players will step into the boots of Kay Vess, a scoundrel with dreams of freedom and starting anew. Alongside her companion Nix, Kay will navigate the treacherous waters of the galaxy’s crime syndicates. She will engage in high-stakes missions. These missions either set her free or land her on the list of the galaxy’s most wanted. What sets Star Wars Outlaws apart is its promise of an open-world experience. This is a first for the Star Wars gaming universe. Players can expect to traverse distinct locations across the galaxy. They will encounter both iconic and new environments bustling with life and opportunities. You can race across landscapes on a speeder. You can also engage in thrilling dogfights in the outer rim. Every choice you make will influence Kay’s reputation. It will also impact the unfolding narrative. ### More details about the game itself The game’s mechanics are designed to offer a cinematic and seamless gameplay experience. Combat is a mix of melee attacks and blaster fire, with various firing modes to suit different situations. Kay’s arsenal includes gadgets like a grappling hook and an electronic hacking device. Her companion Nix can scan environments, interact with objects, and assist in combat. For those eager to dive into this adventure, pre-orders are available. They offer exclusive bonuses like the Kessel Runner Pack. This pack includes cosmetic upgrades for both the speeder and Kay’s ship, the Trailblazer. Additionally, a season pass will unlock two narrative expansions post-launch, adding even more depth to the game. Star Wars Outlaws is not just a game. It’s an invitation to live out a scoundrel’s story in a galaxy far, far away. The release is just around the corner. Fans are counting down the days. They can’t wait to take their first steps into this expansive and immersive new world. ## Wrap up So, this is the first look at Star Wars Outlaws. May the Force be with you on this new outlaw adventure. **Categories:** Games **Tags:** games, star-wars **Hashtags:** games, star-wars --- ### [Split SQL script tool](https://puresourcecode.com/dotnet/csharp/split-sql-script-tool/) **Published:** August 21, 2024 **Author:** Enrico **Excerpt:** I have created a simple C# console application as a tool to split big SQL scripts. For example, I had a problem with data seed scripts. **Content:** I have created a simple [C#](https://puresourcecode.com/?category=csharp&s=) console application a split [SQL script](https://puresourcecode.com/?s=sql) tool to split big scripts. For example, I had a problem with data seed scripts. Those scripts are huge because the database has more than a million records. These records need to be added to a new database. Each script is roughly 400Mb and I can’t upload it in the repository in [Azure DevOps](https://puresourcecode.com/?s=devops) for example. ![Split SQL script tool - Example of big SQL to split](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/08/image-8.png?resize=640%2C453&ssl=1)Example of big SQL to splitAlso, I created a repository for the Split SQL script tool. It contains the code in C#. This repository is available on [GitHub](https://github.com/erossini/SplitSQLscript). ![Split SQL script tool in action](https://github.com/user-attachments/assets/c1e77e44-1a64-4612-a9c9-2b654bc9254d)Split SQL script tool in action ## Implementation explained For the implementation, I added 2 libraries: - [Spectre.Console](https://puresourcecode.com/dotnet/net6/beautiful-console-applications-with-spectre-console/) for displaying nicely the progress of splitting and the result files in a table - [System.CommandLine.DragonFruit](https://github.com/dotnet/command-line-api/tree/main) helps in the creation of the help page and manages the parameters. I have to explore the possibility of using only [Spectre.Console](https://puresourcecode.com/dotnet/net6/beautiful-console-applications-with-spectre-console/) that has similar functionality to [System.CommandLine.DragonFruit](https://github.com/dotnet/command-line-api/tree/main). ### System.CommandLine.DragonFruit So, this library extends the `Main` function of the console application. The documentation is available on the [Microsoft documentation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/) but it is old and the library is not maintained. The magic occurs in the `Main` function and its attribute. For example, look at the code: ``` /// /// Split SQL script in multiple files based on the required size. /// /// The SQL script file to split. /// The destination folder. If this is empty or null, /// the new files will be created in the same directory as the original file. /// The maximum bytes limit for the new files. /// if set to true the procedure will add the command GO /// at the end of each file. public static void Main(string file, string? destination, int limit = 10240000, bool addGo = true) ``` Using this package, the application has the `--help` option and it is displayed in a professional way the details. Here the screenshot ![Example of the help in the console - Split SQL script tool](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/08/image-7.png?resize=640%2C342&ssl=1)Example of the help in the console ## Options OptionDescriptionDefaultfileThe SQL script file to split.destinationThe destination folder. If this is empty or null, the new files will be created in the same directory as the original file.limitThe maximum bytes limit for the new files. \[default: 10240000\]10240000add-goIf set to true the procedure will add the command GO at the end of each file.TrueversionShow version information?, h, helpShow help and usage information## Wrap up In conclusion, I hope that the code for a split SQL script tool will be useful for you. Please, use the code and send your PR to improve the project. Happy coding! **Categories:** C#, SQL **Tags:** sql, sql-script **Hashtags:** sql, sql-script --- ### [Display HTML with MAUI Label](https://puresourcecode.com/dotnet/maui/display-html-with-maui-label/) **Published:** August 17, 2024 **Author:** Enrico **Excerpt:** I show how display HTML with NET8 MAUI Label using only the Label attributes. The source code of this post is available on GitHub **Content:** In this post, I show how display HTML with NET8 MAUI Label using only the `Label` attributes. The source code of this post is available on [GitHub](https://github.com/erossini/MAUIStringFormat). ## Scenario For my app, I want to display a sentence. Part of this sentence can be in bold or italics. So, I started to create an example using `FlexLayou`t. First, I created a `ContentView` called `FormattedMessage` with this XAML: ``` ``` ### Code behind Then, in the code for this view, I added the `binding property` ``` using MauiTest.Extensions; using MauiTest.Models; namespace MauiTest; public partial class FormattedMessage : ContentView { public IList? FormattedText { get; set; } public static readonly BindableProperty MessageProperty = BindableProperty.Create(nameof(Message), typeof(IList), typeof(FormattedMessage), null); public string Message { get => (string)GetValue(MessageProperty); set { SetValue(MessageProperty, value); FormattedText = value.GetFormattedMessage(); OnPropertyChanged(nameof(FormattedText)); } } public FormattedMessage() { InitializeComponent(); BindingContext = this; } } ``` ### FormattedText model Now, each part of the sentence I want to display has to be define using this model ``` using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MauiTest.Models { /// /// Class FormattedText. /// public class FormattedText { /// /// Gets or sets the font attributes. /// /// The font attributes. public FontAttributes FontAttributes { get; set; } = FontAttributes.None; /// /// Gets or sets the text. /// /// The text. public string? Text { get; set; } /// /// Gets or sets the color of the text. /// /// The color of the text. public Color TextColor { get; set; } = Colors.Black; } } ``` ### String Extension I split the string in chunks. I defined that when I find a star `*`, I want that part of the string in bold. Using `RegEx`, I split the string and add the part of the string to the `IList`. ``` using MauiTest.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace MauiTest.Extensions { public static class StringExtensions { public static IList GetFormattedMessage(this string message) { List Result = new List(); string pat = @"\*([^\*]*)\*|([^*]+)"; Match match = Regex.Match(message, pat); while (match.Success) { if (match.Groups[1].Success) { Result.Add(new FormattedText() { Text = match.Groups[1].Value, FontAttributes = FontAttributes.Bold }); } if (match.Groups[2].Success) { Result.Add(new FormattedText() { Text = match.Groups[2].Value, FontAttributes = FontAttributes.None }); } match = match.NextMatch(); } return Result; } } } ``` ## Put together Now, putting everything together, the result is not what I expected. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/08/image-4.png?resize=640%2C38&ssl=1)`FlexLayout` is not rendering the words in the correct way. As a test, I change `FlexLayout` with a `VerticalLayout` and I can see every part of the sentence with the correct attributes ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/08/image-5.png?resize=640%2C124&ssl=1)## Solution I didn’t know that now `Label` has a new attribute `TextType` and I can pass HTML code ``` ``` The only thing is to replace the char `texto subrayado. segundo párrafo, donde parte del texto está tachado. ]]> ``` From the C#, I can write ``` Label label = new Label { TextType = TextType.Html, Text = "Este es el primer párrafo con texto subrayado." + "segundo párrafo, donde parte del texto está tachado." }; ``` I hope this can help someone. Happy coding! **Categories:** .NET8, MAUI **Tags:** maui, net8 **Hashtags:** maui, net8 --- ### [New MarkdownEditor components for JavaScript and Blazor](https://puresourcecode.com/dotnet/csharp/new-markdowneditor-components-for-javascript-and-blazor/) **Published:** July 1, 2024 **Author:** Enrico **Excerpt:** Today, after 2 years, I released a new Markdown Editor components for JavaScript and Blazor. The full source code is available on GitHub. **Content:** Today, after 2 years, I released a new MarkdownEditor components for JavaScript and Blazor. In 2022, I created a [Markdown Editor for Blazor](https://puresourcecode.com/dotnet/blazor/markdown-editor-component-for-blazor/) based on EasyMDE, a JavaScript code that implements a markdown editor. EasyMDE is a wrap on SimpleMDE another JavaScript code. ![New MarkdownEditor components for JavaScript and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/07/51319377-26fe6e00-1a5d-11e9-8cc6-3137a566796d.png?w=640&ssl=1) Because I found some bugs in the scripts and the projects were not maintained anymore, I decided to continue this great JavaScript code and, at the same time, improve my component for Blazor. Now, you can find the source code and help me to improve the projects here: - [Easy Markdown Editor for JavaScript](https://github.com/erossini/EasyMarkdownEditor): a simple, beautiful, and embeddable JavaScript Markdown editor. Delightful editing for beginners and experts alike. Features built-in autosaving and spell-checking. - [Markdown Editor for Blazor](https://github.com/erossini/BlazorMarkdownEditor): this is the component for Blazor based on the previous project - [Try now](http://markdown.puresourcecode.com/) my project ## How to use the JavaScript code First, you have to install the package from [npm](https://www.npmjs.com/package/psc-markdowneditor): ``` npm install psc-markdowneditor ``` Another option is to add the required scripts in your HTML using `UNPKG`: ``` ``` or via jsDelivr ``` ``` Then, add a `textarea` in your HTML code like that: ``` ``` Alternatively, you can select a specific `textarea`, via JavaScript: ``` ``` ## Editor functions Use `easyMDE.value()` to get the content of the editor: ``` ``` Use `easyMDE.value(val)` to set the content of the editor: ``` ``` ## Options - **autoDownloadFontAwesome**: If set to `true`, force downloads Font Awesome (used for icons). If set to `false`, prevents downloading. Defaults to `undefined`, which will intelligently check whether Font Awesome has already been included, then download accordingly. - **autofocus**: If set to `true`, focuses the editor automatically. Defaults to `false`. - **autosave**: *Saves the text that’s being written and will load it back in the future. It will forget the text when the form it’s contained in is submitted.* - **enabled**: If set to `true`, saves the text automatically. Defaults to `false`. - **delay**: Delay between saves, in milliseconds. Defaults to `10000` (10 seconds). - **submit\_delay**: Delay before assuming that submit of the form failed and saving the text, in milliseconds. Defaults to `autosave.delay` or `10000` (10 seconds). - **uniqueId**: You must set a unique string identifier so that EasyMDE can autosave. Something that separates this from other instances of EasyMDE elsewhere on your website. - **timeFormat**: Set DateTimeFormat. More information see [DateTimeFormat instances](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat). Default `locale: en-US, format: hour:minute`. - **text**: Set text for autosave. - **autoRefresh**: Useful, when initializing the editor in a hidden DOM node. If set to `{ delay: 300 }`, it will check every 300 ms if the editor is visible and if positive, call CodeMirror’s [`refresh()`](https://codemirror.net/doc/manual.html#refresh). - **blockStyles**: Customize how certain buttons that style blocks of text behave. - **bold**: Can be set to `**` or `__`. Defaults to `**`. - **code**: Can be set to ````` or `~~~`. Defaults to `````. - **italic**: Can be set to `*` or `_`. Defaults to `*`. - **unorderedListStyle**: can be `*`, `-` or `+`. Defaults to `*`. - **scrollbarStyle**: Chooses a scrollbar implementation. The default is “native”, showing native scrollbars. The core library also provides the “null” style, which completely hides the scrollbars. Addons can implement additional scrollbar models. - **element**: The DOM element for the `textarea` element to use. Defaults to the first `textarea` element on the page. - **forceSync**: If set to `true`, force text changes made in EasyMDE to be immediately stored in original text area. Defaults to `false`. - **hideIcons**: An array of icon names to hide. Can be used to hide specific icons shown by default without completely customizing the toolbar. - **indentWithTabs**: If set to `false`, indent using spaces instead of tabs. Defaults to `true`. - **initialValue**: If set, will customize the initial value of the editor. - **previewImagesInEditor**: – EasyMDE will show preview of images, `false` by default, preview for images will appear only for images on separate lines. - **imagesPreviewHandler**: – A custom function for handling the preview of images. Takes the parsed string between the parantheses of the image markdown `![]( )` as argument and returns a string that serves as the `src` attribute of the `` tag in the preview. Enables dynamic previewing of images in the frontend without having to upload them to a server, allows copy-pasting of images to the editor with preview. - **insertTexts**: Customize how certain buttons that insert text behave. Takes an array with two elements. The first element will be the text inserted before the cursor or highlight, and the second element will be inserted after. For example, this is the default link value: `["[", "](http://)"]`. - horizontalRule - image - link - table - **lineNumbers**: If set to `true`, enables line numbers in the editor. - **lineWrapping**: If set to `false`, disable line wrapping. Defaults to `true`. - **minHeight**: Sets the minimum height for the composition area, before it starts auto-growing. Should be a string containing a valid CSS value like `"500px"`. Defaults to `"300px"`. - **maxHeight**: Sets fixed height for the composition area. `minHeight` option will be ignored. Should be a string containing a valid CSS value like `"500px"`. Defaults to `undefined`. - **onToggleFullScreen**: A function that gets called when the editor’s full screen mode is toggled. The function will be passed a boolean as parameter, `true` when the editor is currently going into full screen mode, or `false`. - **parsingConfig**: Adjust settings for parsing the Markdown during editing (not previewing). - **allowAtxHeaderWithoutSpace**: If set to `true`, will render headers without a space after the `#`. Defaults to `false`. - **strikethrough**: If set to `false`, will not process GFM strikethrough syntax. Defaults to `true`. - **underscoresBreakWords**: If set to `true`, let underscores be a delimiter for separating words. Defaults to `false`. - **overlayMode**: Pass a custom codemirror [overlay mode](https://codemirror.net/doc/manual.html#modeapi) to parse and style the Markdown during editing. - **mode**: A codemirror mode object. - **combine**: If set to `false`, will *replace* CSS classes returned by the default Markdown mode. Otherwise the classes returned by the custom mode will be combined with the classes returned by the default mode. Defaults to `true`. - **placeholder**: If set, displays a custom placeholder message. - **previewClass**: A string or array of strings that will be applied to the preview screen when activated. Defaults to `"editor-preview"`. - **previewRender**: Custom function for parsing the plaintext Markdown and returning HTML. Used when user previews. - **promptURLs**: If set to `true`, a JS alert window appears asking for the link or image URL. Defaults to `false`. - **promptTexts**: Customize the text used to prompt for URLs. - **image**: The text to use when prompting for an image’s URL. Defaults to `URL of the image:`. - **link**: The text to use when prompting for a link’s URL. Defaults to `URL for the link:`. - **iconClassMap**: Used to specify the icon class names for the various toolbar buttons. - **uploadImage**: If set to `true`, enables the image upload functionality, which can be triggered by drag and drop, copy-paste and through the browse-file window (opened when the user click on the *upload-image* icon). Defaults to `false`. - **imageMaxSize**: Maximum image size in bytes, checked before upload (note: never trust client, always check the image size at server-side). Defaults to `1024 * 1024 * 2` (2 MB). - **imageAccept**: A comma-separated list of mime-types used to check image type before upload (note: never trust client, always check file types at server-side). Defaults to `image/png, image/jpeg`. - **imageUploadFunction**: A custom function for handling the image upload. Using this function will render the options `imageMaxSize`, `imageAccept`, `imageUploadEndpoint` and `imageCSRFToken` ineffective. - The function gets a file and `onSuccess` and `onError` callback functions as parameters. `onSuccess(imageUrl: string)` and `onError(errorMessage: string)` - **imageUploadEndpoint**: The endpoint where the images data will be sent, via an asynchronous *POST* request. The server is supposed to save this image, and return a JSON response. - if the request was successfully processed (HTTP 200 OK): `{"data": {"filePath": ""}}` where *filePath* is the path of the image (absolute if `imagePathAbsolute` is set to true, relative if otherwise); - otherwise: `{"error": ""}`, where *errorCode* can be `noFileGiven` (HTTP 400 Bad Request), `typeNotAllowed` (HTTP 415 Unsupported Media Type), `fileTooLarge` (HTTP 413 Payload Too Large) or `importError` (see *errorMessages* below). If *errorCode* is not one of the *errorMessages*, it is alerted unchanged to the user. This allows for server-side error messages. No default value. - **imagePathAbsolute**: If set to `true`, will treat `imageUrl` from `imageUploadFunction` and *filePath* returned from `imageUploadEndpoint` as an absolute rather than relative path, i.e. not prepend `window.location.origin` to it. - **imageCSRFToken**: CSRF token to include with AJAX call to upload image. For various instances like Django, Spring and Laravel. - **imageCSRFName**: CSRF token filed name to include with AJAX call to upload image, applied when `imageCSRFToken` has value, defaults to `csrfmiddlewaretoken`. - **imageCSRFHeader**: If set to `true`, passing CSRF token via header. Defaults to `false`, which pass CSRF through request body. - **imageTexts**: Texts displayed to the user (mainly on the status bar) for the import image feature, where `#image_name#`, `#image_size#` and `#image_max_size#` will replaced by their respective values, that can be used for customization or internationalization: - **sbInit**: Status message displayed initially if `uploadImage` is set to `true`. Defaults to `Attach files by drag and dropping or pasting from clipboard.`. - **sbOnDragEnter**: Status message displayed when the user drags a file to the text area. Defaults to `Drop image to upload it.`. - **sbOnDrop**: Status message displayed when the user drops a file in the text area. Defaults to `Uploading images #images_names#`. - **sbProgress**: Status message displayed to show uploading progress. Defaults to `Uploading #file_name#: #progress#%`. - **sbOnUploaded**: Status message displayed when the image has been uploaded. Defaults to `Uploaded #image_name#`. - **sizeUnits**: A comma-separated list of units used to display messages with human-readable file sizes. Defaults to `B, KB, MB` (example: `218 KB`). You can use `B,KB,MB` instead if you prefer without whitespaces (`218KB`). - **errorMessages**: Errors displayed to the user, using the `errorCallback` option, where `#image_name#`, `#image_size#` and `#image_max_size#` will replaced by their respective values, that can be used for customization or internationalization: - **noFileGiven**: The server did not receive any file from the user. Defaults to `You must select a file.`. - **typeNotAllowed**: The user send a file type which doesn’t match the `imageAccept` list, or the server returned this error code. Defaults to `This image type is not allowed.`. - **fileTooLarge**: The size of the image being imported is bigger than the `imageMaxSize`, or if the server returned this error code. Defaults to `Image #image_name# is too big (#image_size#).\nMaximum file size is #image_max_size#.`. - **importError**: An unexpected error occurred when uploading the image. Defaults to `Something went wrong when uploading the image #image_name#.`. - **errorCallback**: A callback function used to define how to display an error message. Defaults to `(errorMessage) => alert(errorMessage)`. - **renderingConfig**: Adjust settings for parsing the Markdown during previewing (not editing). - **codeSyntaxHighlighting**: If set to `true`, will highlight using [highlight.js](https://github.com/isagalaev/highlight.js). Defaults to `false`. To use this feature you must include highlight.js on your page or pass in using the `hljs` option. For example, include the script and the CSS files like: `` `` - **hljs**: An injectible instance of [highlight.js](https://github.com/isagalaev/highlight.js). If you don’t want to rely on the global namespace (`window.hljs`), you can provide an instance here. Defaults to `undefined`. - **markedOptions**: Set the internal Markdown renderer’s [options](https://marked.js.org/#/USING_ADVANCED.md#options). Other `renderingConfig` options will take precedence. - **singleLineBreaks**: If set to `false`, disable parsing [GitHub Flavored Markdown](https://github.github.com/gfm/) (GFM) single line breaks. Defaults to `true`. - **sanitizerFunction**: Custom function for sanitizing the HTML output of Markdown renderer. - **shortcuts**: Keyboard shortcuts associated with this instance. Defaults to the [array of shortcuts](#keyboard-shortcuts). - **showIcons**: An array of icon names to show. Can be used to show specific icons hidden by default without completely customizing the toolbar. - **spellChecker**: If set to `false`, disable the spell checker. Defaults to `true`. Optionally pass a CodeMirrorSpellChecker-compliant function. - **inputStyle**: `textarea` or `contenteditable`. Defaults to `textarea` for desktop and `contenteditable` for mobile. `contenteditable` option is necessary to enable nativeSpellcheck. - **nativeSpellcheck**: If set to `false`, disable native spell checker. Defaults to `true`. - **sideBySideFullscreen**: If set to `false`, allows side-by-side editing without going into fullscreen. Defaults to `true`. - **status**: If set to `false`, hide the status bar. Defaults to the array of built-in status bar items. - Optionally, you can set an array of status bar items to include, and in what order. You can even define your own custom status bar items. - **styleSelectedText**: If set to `false`, remove the `CodeMirror-selectedtext` class from selected lines. Defaults to `true`. - **syncSideBySidePreviewScroll**: If set to `false`, disable syncing scroll in side by side mode. Defaults to `true`. - **tabSize**: If set, customize the tab size. Defaults to `2`. - **theme**: Override the theme. Defaults to `easymde`. - **toolbar**: If set to `false`, hide the toolbar. Defaults to the [array of icons](#toolbar-icons). - **toolbarTips**: If set to `false`, disable toolbar button tips. Defaults to `true`. - **toolbarButtonClassPrefix**: Adds a prefix to the toolbar button classes when set. For example, a value of `"mde"` results in `"mde-bold"` for the Bold button. - **direction**: `rtl` or `ltr`. Changes text direction to support right-to-left languages. Defaults to `ltr`. ## Toolbar icons Below are the built-in toolbar icons (only some of which are enabled by default), which can be reorganized however you like. “Name” is the name of the icon, referenced in the JavaScript. “Action” is either a function or a URL to open. “Class” is the class given to the icon. “Tooltip” is the small tooltip that appears via the `title=""` attribute. Note that shortcut hints are added automatically and reflect the specified action if it has a key bind assigned to it (i.e. with the value of `action` set to `bold` and that of `tooltip` set to `Bold`, the final text the user will see would be “Bold (Ctrl-B)”). Additionally, you can add a separator between any icons by adding `"|"` to the toolbar array. NameActionTooltip ClassboldtoggleBoldBold fa fa-bolditalictoggleItalicItalic fa fa-italicstrikethroughtoggleStrikethroughStrikethrough fa fa-strikethroughheadingtoggleHeadingSmallerHeading fa fa-headerheading-smallertoggleHeadingSmallerSmaller Heading fa fa-headerheading-biggertoggleHeadingBiggerBigger Heading fa fa-lg fa-headerheading-1toggleHeading1Big Heading fa fa-header header-1heading-2toggleHeading2Medium Heading fa fa-header header-2heading-3toggleHeading3Small Heading fa fa-header header-3codetoggleCodeBlockCode fa fa-codequotetoggleBlockquoteQuote fa fa-quote-leftunordered-listtoggleUnorderedListGeneric List fa fa-list-ulordered-listtoggleOrderedListNumbered List fa fa-list-olclean-blockcleanBlockClean block fa fa-eraserlinkdrawLinkCreate Link fa fa-linkimagedrawImageInsert Image fa fa-picture-oupload-imagedrawUploadedImageRaise browse-file window fa fa-imagetabledrawTableInsert Table fa fa-tablehorizontal-ruledrawHorizontalRuleInsert Horizontal Line fa fa-minuspreviewtogglePreviewToggle Preview fa fa-eye no-disableside-by-sidetoggleSideBySideToggle Side by Side fa fa-columns no-disable no-mobilefullscreentoggleFullScreenToggle Fullscreen fa fa-arrows-alt no-disable no-mobileguide[This link](https://www.markdownguide.org/basic-syntax/)Markdown Guide fa fa-question-circleundoundoUndo fa fa-undoredoredoRedo fa fa-redoindentindentIndent fa fa-indentoutdentoutdentOutdent fa fa-outdent## Keyboard shortcuts EasyMDE comes with an array of predefined keyboard shortcuts, but they can be altered with a configuration option. The list of default ones is as follows: Shortcut (Windows / Linux)Shortcut (macOS)ActionCtrl–'Cmd–'“toggleBlockquote”Ctrl–BCmd–B“toggleBold”Ctrl–ECmd–E“cleanBlock”Ctrl–HCmd–H“toggleHeadingSmaller”Ctrl–ICmd–I“toggleItalic”Ctrl–KCmd–K“drawLink”Ctrl–LCmd–L“toggleUnorderedList”Ctrl–PCmd–P“togglePreview”Ctrl–Alt–CCmd–Alt–C“toggleCodeBlock”Ctrl–Alt–ICmd–Alt–I“drawImage”Ctrl–Alt–LCmd–Alt–L“toggleOrderedList”Shift–Ctrl–HShift–Cmd–H“toggleHeadingBigger”F9F9“toggleSideBySide”F11F11“toggleFullScreen”Ctrl–Alt–1Cmd–Alt–1“toggleHeading1”Ctrl–Alt–2Cmd–Alt–2“toggleHeading2”Ctrl–Alt–3Cmd–Alt–3“toggleHeading3”Ctrl–Alt–4Cmd–Alt–4“toggleHeading4”Ctrl–Alt–5Cmd–Alt–5“toggleHeading5”Ctrl–Alt–6Cmd–Alt–6“toggleHeading6” **Categories:** .NET8, Blazor, C#, JavaScript **Tags:** blazor, blazor-server, blazor-webassembly, javascript, markdown --- ### [Country data library for NET8](https://puresourcecode.com/dotnet/csharp/country-data-library-for-net8/) **Published:** June 19, 2024 **Author:** Enrico **Excerpt:** I'm going to release a simple cross-platform offline country data library for NET8. The library is based on the ISO-3166-1 standard **Content:** Today, I’m going to release a simple cross-platform offline country data library for NET8. The library is based on the ISO-3166-1 standard. Based on the operating system, using *Unicode*\*, you can display the flag of the country. On Windows, the *emoji* displays the shortcode of the country, not the flag. The library has the flags of the countries in SVG format. The flags can be displayed in two different types: `Square` and `Wide`. The source code of the library is available on [GitHub](https://github.com/erossini/CSharpCountryData). ## Install Library To install the library in your project, open the *Package Manager* and type the following command: ``` Install-Package PSC.CSharp.CountryData ``` Also, you can use the .NET CLI to install the library. Type the following command: ``` dotnet add package PSC.CSharp.CountryData ``` ## Usage Here is an example how to use the library in your project. ### Initialize the CountryData ``` // loads all Country Data via the constructor (You can initialize this once as a singleton) var countryHelper = new CountryHelper(); ``` ### Get the list of countries ``` foreach (var country in countries) Console.WriteLine(country); ``` ### Get the list of regions in a country by country code ``` var regions = countryHelper.GetRegionByCountryCode("IT"); foreach (var region in regions) Console.WriteLine(region.Name); ``` ### Using lambda for custom queries `GetCountryData()` returns an `IEnumerable` which can be queried with Lambda for a more flexible usage. ``` var data = countryHelper.GetCountryData(); ``` ### Get the list of countries by their Names This code displays the name of the countries using a `foreach`. ``` var countries = data.Select(c => c.CountryName).ToList(); foreach (var country in countries) Console.WriteLine(country); ``` Here the code to use `Linq` to filter the list. ``` data.Where(x => x.CountryShortCode == "US") .Select(r=>r.Regions).FirstOrDefault() .ToList(); ``` ### Get the flag for a country In the library, I added a method to get the flag of a country. The flags are in SVG format. The type of the flag can be `Square` or `Wide`. ``` var flag = helper.GetFlagByCountryCode("GB", FlagType.Square); ``` ## List of supported ISO-3166-1 country codes Here is the list of supported ISO-3166-1 country codes, their code point pairs and `Emoji` flags. Based on the operating system, you see different *Emoji*. In Windows, you see the short name of the country. In iOS and Android, the flag of the correspondent country is displayed. ISOEmojiUnicodeNameAD🇦🇩U+1F1E6 U+1F1E9AndorraAE🇦🇪U+1F1E6 U+1F1EAUnited Arab EmiratesAF🇦🇫U+1F1E6 U+1F1EBAfghanistanAG🇦🇬U+1F1E6 U+1F1ECAntigua and BarbudaAI🇦🇮U+1F1E6 U+1F1EEAnguillaAL🇦🇱U+1F1E6 U+1F1F1AlbaniaAM🇦🇲U+1F1E6 U+1F1F2ArmeniaAO🇦🇴U+1F1E6 U+1F1F4AngolaAQ🇦🇶U+1F1E6 U+1F1F6AntarcticaAR🇦🇷U+1F1E6 U+1F1F7ArgentinaAS🇦🇸U+1F1E6 U+1F1F8American SamoaAT🇦🇹U+1F1E6 U+1F1F9AustriaAU🇦🇺U+1F1E6 U+1F1FAAustraliaAW🇦🇼U+1F1E6 U+1F1FCArubaAX🇦🇽U+1F1E6 U+1F1FDÅland IslandsAZ🇦🇿U+1F1E6 U+1F1FFAzerbaijanBA🇧🇦U+1F1E7 U+1F1E6Bosnia and HerzegovinaBB🇧🇧U+1F1E7 U+1F1E7BarbadosBD🇧🇩U+1F1E7 U+1F1E9BangladeshBE🇧🇪U+1F1E7 U+1F1EABelgiumBF🇧🇫U+1F1E7 U+1F1EBBurkina FasoBG🇧🇬U+1F1E7 U+1F1ECBulgariaBH🇧🇭U+1F1E7 U+1F1EDBahrainBI🇧🇮U+1F1E7 U+1F1EEBurundiBJ🇧🇯U+1F1E7 U+1F1EFBeninBL🇧🇱U+1F1E7 U+1F1F1Saint BarthélemyBM🇧🇲U+1F1E7 U+1F1F2BermudaBN🇧🇳U+1F1E7 U+1F1F3Brunei DarussalamBO🇧🇴U+1F1E7 U+1F1F4BoliviaBQ🇧🇶U+1F1E7 U+1F1F6Bonaire, Sint Eustatius and SabaBR🇧🇷U+1F1E7 U+1F1F7BrazilBS🇧🇸U+1F1E7 U+1F1F8BahamasBT🇧🇹U+1F1E7 U+1F1F9BhutanBV🇧🇻U+1F1E7 U+1F1FBBouvet IslandBW🇧🇼U+1F1E7 U+1F1FCBotswanaBY🇧🇾U+1F1E7 U+1F1FEBelarusBZ🇧🇿U+1F1E7 U+1F1FFBelizeCA🇨🇦U+1F1E8 U+1F1E6CanadaCC🇨🇨U+1F1E8 U+1F1E8Cocos (Keeling) IslandsCD🇨🇩U+1F1E8 U+1F1E9CongoCF🇨🇫U+1F1E8 U+1F1EBCentral African RepublicCG🇨🇬U+1F1E8 U+1F1ECCongoCH🇨🇭U+1F1E8 U+1F1EDSwitzerlandCI🇨🇮U+1F1E8 U+1F1EECôte D’IvoireCK🇨🇰U+1F1E8 U+1F1F0Cook IslandsCL🇨🇱U+1F1E8 U+1F1F1ChileCM🇨🇲U+1F1E8 U+1F1F2CameroonCN🇨🇳U+1F1E8 U+1F1F3ChinaCO🇨🇴U+1F1E8 U+1F1F4ColombiaCR🇨🇷U+1F1E8 U+1F1F7Costa RicaCU🇨🇺U+1F1E8 U+1F1FACubaCV🇨🇻U+1F1E8 U+1F1FBCape VerdeCW🇨🇼U+1F1E8 U+1F1FCCuraçaoCX🇨🇽U+1F1E8 U+1F1FDChristmas IslandCY🇨🇾U+1F1E8 U+1F1FECyprusCZ🇨🇿U+1F1E8 U+1F1FFCzech RepublicDE🇩🇪U+1F1E9 U+1F1EAGermanyDJ🇩🇯U+1F1E9 U+1F1EFDjiboutiDK🇩🇰U+1F1E9 U+1F1F0DenmarkDM🇩🇲U+1F1E9 U+1F1F2DominicaDO🇩🇴U+1F1E9 U+1F1F4Dominican RepublicDZ🇩🇿U+1F1E9 U+1F1FFAlgeriaEC🇪🇨U+1F1EA U+1F1E8EcuadorEE🇪🇪U+1F1EA U+1F1EAEstoniaEG🇪🇬U+1F1EA U+1F1ECEgyptEH🇪🇭U+1F1EA U+1F1EDWestern SaharaER🇪🇷U+1F1EA U+1F1F7EritreaES🇪🇸U+1F1EA U+1F1F8SpainET🇪🇹U+1F1EA U+1F1F9EthiopiaFI🇫🇮U+1F1EB U+1F1EEFinlandFJ🇫🇯U+1F1EB U+1F1EFFijiFK🇫🇰U+1F1EB U+1F1F0Falkland Islands (Malvinas)FM🇫🇲U+1F1EB U+1F1F2MicronesiaFO🇫🇴U+1F1EB U+1F1F4Faroe IslandsFR🇫🇷U+1F1EB U+1F1F7FranceGA🇬🇦U+1F1EC U+1F1E6GabonGB🇬🇧U+1F1EC U+1F1E7United KingdomGD🇬🇩U+1F1EC U+1F1E9GrenadaGE🇬🇪U+1F1EC U+1F1EAGeorgiaGF🇬🇫U+1F1EC U+1F1EBFrench GuianaGG🇬🇬U+1F1EC U+1F1ECGuernseyGH🇬🇭U+1F1EC U+1F1EDGhanaGI🇬🇮U+1F1EC U+1F1EEGibraltarGL🇬🇱U+1F1EC U+1F1F1GreenlandGM🇬🇲U+1F1EC U+1F1F2GambiaGN🇬🇳U+1F1EC U+1F1F3GuineaGP🇬🇵U+1F1EC U+1F1F5GuadeloupeGQ🇬🇶U+1F1EC U+1F1F6Equatorial GuineaGR🇬🇷U+1F1EC U+1F1F7GreeceGS🇬🇸U+1F1EC U+1F1F8South GeorgiaGT🇬🇹U+1F1EC U+1F1F9GuatemalaGU🇬🇺U+1F1EC U+1F1FAGuamGW🇬🇼U+1F1EC U+1F1FCGuinea-BissauGY🇬🇾U+1F1EC U+1F1FEGuyanaHK🇭🇰U+1F1ED U+1F1F0Hong KongHM🇭🇲U+1F1ED U+1F1F2Heard Island and Mcdonald IslandsHN🇭🇳U+1F1ED U+1F1F3HondurasHR🇭🇷U+1F1ED U+1F1F7CroatiaHT🇭🇹U+1F1ED U+1F1F9HaitiHU🇭🇺U+1F1ED U+1F1FAHungaryID🇮🇩U+1F1EE U+1F1E9IndonesiaIE🇮🇪U+1F1EE U+1F1EAIrelandIL🇮🇱U+1F1EE U+1F1F1IsraelIM🇮🇲U+1F1EE U+1F1F2Isle of ManIN🇮🇳U+1F1EE U+1F1F3IndiaIO🇮🇴U+1F1EE U+1F1F4British Indian Ocean TerritoryIQ🇮🇶U+1F1EE U+1F1F6IraqIR🇮🇷U+1F1EE U+1F1F7IranIS🇮🇸U+1F1EE U+1F1F8IcelandIT🇮🇹U+1F1EE U+1F1F9ItalyJE🇯🇪U+1F1EF U+1F1EAJerseyJM🇯🇲U+1F1EF U+1F1F2JamaicaJO🇯🇴U+1F1EF U+1F1F4JordanJP🇯🇵U+1F1EF U+1F1F5JapanKE🇰🇪U+1F1F0 U+1F1EAKenyaKG🇰🇬U+1F1F0 U+1F1ECKyrgyzstanKH🇰🇭U+1F1F0 U+1F1EDCambodiaKI🇰🇮U+1F1F0 U+1F1EEKiribatiKM🇰🇲U+1F1F0 U+1F1F2ComorosKN🇰🇳U+1F1F0 U+1F1F3Saint Kitts and NevisKP🇰🇵U+1F1F0 U+1F1F5North KoreaKR🇰🇷U+1F1F0 U+1F1F7South KoreaKW🇰🇼U+1F1F0 U+1F1FCKuwaitKY🇰🇾U+1F1F0 U+1F1FECayman IslandsKZ🇰🇿U+1F1F0 U+1F1FFKazakhstanLA🇱🇦U+1F1F1 U+1F1E6Lao People’s Democratic RepublicLB🇱🇧U+1F1F1 U+1F1E7LebanonLC🇱🇨U+1F1F1 U+1F1E8Saint LuciaLI🇱🇮U+1F1F1 U+1F1EELiechtensteinLK🇱🇰U+1F1F1 U+1F1F0Sri LankaLR🇱🇷U+1F1F1 U+1F1F7LiberiaLS🇱🇸U+1F1F1 U+1F1F8LesothoLT🇱🇹U+1F1F1 U+1F1F9LithuaniaLU🇱🇺U+1F1F1 U+1F1FALuxembourgLV🇱🇻U+1F1F1 U+1F1FBLatviaLY🇱🇾U+1F1F1 U+1F1FELibyaMA🇲🇦U+1F1F2 U+1F1E6MoroccoMC🇲🇨U+1F1F2 U+1F1E8MonacoMD🇲🇩U+1F1F2 U+1F1E9MoldovaME🇲🇪U+1F1F2 U+1F1EAMontenegroMF🇲🇫U+1F1F2 U+1F1EBSaint Martin (French Part)MG🇲🇬U+1F1F2 U+1F1ECMadagascarMH🇲🇭U+1F1F2 U+1F1EDMarshall IslandsMK🇲🇰U+1F1F2 U+1F1F0MacedoniaML🇲🇱U+1F1F2 U+1F1F1MaliMM🇲🇲U+1F1F2 U+1F1F2MyanmarMN🇲🇳U+1F1F2 U+1F1F3MongoliaMO🇲🇴U+1F1F2 U+1F1F4MacaoMP🇲🇵U+1F1F2 U+1F1F5Northern Mariana IslandsMQ🇲🇶U+1F1F2 U+1F1F6MartiniqueMR🇲🇷U+1F1F2 U+1F1F7MauritaniaMS🇲🇸U+1F1F2 U+1F1F8MontserratMT🇲🇹U+1F1F2 U+1F1F9MaltaMU🇲🇺U+1F1F2 U+1F1FAMauritiusMV🇲🇻U+1F1F2 U+1F1FBMaldivesMW🇲🇼U+1F1F2 U+1F1FCMalawiMX🇲🇽U+1F1F2 U+1F1FDMexicoMY🇲🇾U+1F1F2 U+1F1FEMalaysiaMZ🇲🇿U+1F1F2 U+1F1FFMozambiqueNA🇳🇦U+1F1F3 U+1F1E6NamibiaNC🇳🇨U+1F1F3 U+1F1E8New CaledoniaNE🇳🇪U+1F1F3 U+1F1EANigerNF🇳🇫U+1F1F3 U+1F1EBNorfolk IslandNG🇳🇬U+1F1F3 U+1F1ECNigeriaNI🇳🇮U+1F1F3 U+1F1EENicaraguaNL🇳🇱U+1F1F3 U+1F1F1NetherlandsNO🇳🇴U+1F1F3 U+1F1F4NorwayNP🇳🇵U+1F1F3 U+1F1F5NepalNR🇳🇷U+1F1F3 U+1F1F7NauruNU🇳🇺U+1F1F3 U+1F1FANiueNZ🇳🇿U+1F1F3 U+1F1FFNew ZealandOM🇴🇲U+1F1F4 U+1F1F2OmanPA🇵🇦U+1F1F5 U+1F1E6PanamaPE🇵🇪U+1F1F5 U+1F1EAPeruPF🇵🇫U+1F1F5 U+1F1EBFrench PolynesiaPG🇵🇬U+1F1F5 U+1F1ECPapua New GuineaPH🇵🇭U+1F1F5 U+1F1EDPhilippinesPK🇵🇰U+1F1F5 U+1F1F0PakistanPL🇵🇱U+1F1F5 U+1F1F1PolandPM🇵🇲U+1F1F5 U+1F1F2Saint Pierre and MiquelonPN🇵🇳U+1F1F5 U+1F1F3PitcairnPR🇵🇷U+1F1F5 U+1F1F7Puerto RicoPS🇵🇸U+1F1F5 U+1F1F8Palestinian TerritoryPT🇵🇹U+1F1F5 U+1F1F9PortugalPW🇵🇼U+1F1F5 U+1F1FCPalauPY🇵🇾U+1F1F5 U+1F1FEParaguayQA🇶🇦U+1F1F6 U+1F1E6QatarRE🇷🇪U+1F1F7 U+1F1EARéunionRO🇷🇴U+1F1F7 U+1F1F4RomaniaRS🇷🇸U+1F1F7 U+1F1F8SerbiaRU🇷🇺U+1F1F7 U+1F1FARussiaRW🇷🇼U+1F1F7 U+1F1FCRwandaSA🇸🇦U+1F1F8 U+1F1E6Saudi ArabiaSB🇸🇧U+1F1F8 U+1F1E7Solomon IslandsSC🇸🇨U+1F1F8 U+1F1E8SeychellesSD🇸🇩U+1F1F8 U+1F1E9SudanSE🇸🇪U+1F1F8 U+1F1EASwedenSG🇸🇬U+1F1F8 U+1F1ECSingaporeSH🇸🇭U+1F1F8 U+1F1EDSaint Helena, Ascension and Tristan Da CunhaSI🇸🇮U+1F1F8 U+1F1EESloveniaSJ🇸🇯U+1F1F8 U+1F1EFSvalbard and Jan MayenSK🇸🇰U+1F1F8 U+1F1F0SlovakiaSL🇸🇱U+1F1F8 U+1F1F1Sierra LeoneSM🇸🇲U+1F1F8 U+1F1F2San MarinoSN🇸🇳U+1F1F8 U+1F1F3SenegalSO🇸🇴U+1F1F8 U+1F1F4SomaliaSR🇸🇷U+1F1F8 U+1F1F7SurinameSS🇸🇸U+1F1F8 U+1F1F8South SudanST🇸🇹U+1F1F8 U+1F1F9Sao Tome and PrincipeSV🇸🇻U+1F1F8 U+1F1FBEl SalvadorSX🇸🇽U+1F1F8 U+1F1FDSint Maarten (Dutch Part)SY🇸🇾U+1F1F8 U+1F1FESyrian Arab RepublicSZ🇸🇿U+1F1F8 U+1F1FFSwazilandTC🇹🇨U+1F1F9 U+1F1E8Turks and Caicos IslandsTD🇹🇩U+1F1F9 U+1F1E9ChadTF🇹🇫U+1F1F9 U+1F1EBFrench Southern TerritoriesTG🇹🇬U+1F1F9 U+1F1ECTogoTH🇹🇭U+1F1F9 U+1F1EDThailandTJ🇹🇯U+1F1F9 U+1F1EFTajikistanTK🇹🇰U+1F1F9 U+1F1F0TokelauTL🇹🇱U+1F1F9 U+1F1F1Timor-LesteTM🇹🇲U+1F1F9 U+1F1F2TurkmenistanTN🇹🇳U+1F1F9 U+1F1F3TunisiaTO🇹🇴U+1F1F9 U+1F1F4TongaTR🇹🇷U+1F1F9 U+1F1F7TurkeyTT🇹🇹U+1F1F9 U+1F1F9Trinidad and TobagoTV🇹🇻U+1F1F9 U+1F1FBTuvaluTW🇹🇼U+1F1F9 U+1F1FCTaiwanTZ🇹🇿U+1F1F9 U+1F1FFTanzaniaUA🇺🇦U+1F1FA U+1F1E6UkraineUG🇺🇬U+1F1FA U+1F1ECUgandaUM🇺🇲U+1F1FA U+1F1F2United States Minor Outlying IslandsUS🇺🇸U+1F1FA U+1F1F8United StatesUY🇺🇾U+1F1FA U+1F1FEUruguayUZ🇺🇿U+1F1FA U+1F1FFUzbekistanVA🇻🇦U+1F1FB U+1F1E6Vatican CityVC🇻🇨U+1F1FB U+1F1E8Saint Vincent and The GrenadinesVE🇻🇪U+1F1FB U+1F1EAVenezuelaVG🇻🇬U+1F1FB U+1F1ECVirgin Islands, BritishVI🇻🇮U+1F1FB U+1F1EEVirgin Islands, U.S.VN🇻🇳U+1F1FB U+1F1F3Viet NamVU🇻🇺U+1F1FB U+1F1FAVanuatuWF🇼🇫U+1F1FC U+1F1EBWallis and FutunaWS🇼🇸U+1F1FC U+1F1F8SamoaYE🇾🇪U+1F1FE U+1F1EAYemenYT🇾🇹U+1F1FE U+1F1F9MayotteZA🇿🇦U+1F1FF U+1F1E6South AfricaZM🇿🇲U+1F1FF U+1F1F2ZambiaZW🇿🇼U+1F1FF U+1F1FCZimbabwe## Functions FunctionReturnDescriptionGetCountries()IEnumerableGets the list of all countries in the worldGetCountryByCodeCountryReturns a single Country Data by ShortCodeGetCountryData()IEnumerableGets the list of all countries in the world with their dataGetCountryEmojiFlagstringReturns the Emoji Flag of a country by ShortCodeGetRegionByCountryCodeListReturns the Regions of a country by ShortCodeGetJsonDatastringReturns the JSON Data of all countries in the world### Flags FunctionReturnDescriptionGetFlagByCountryCodestringReturns the Flag of a country by ShortCodeGetFlagDataFlagModelReturn the `FlagModel` of one country by ShortCodeGetNameByCountryCodestringReturns the Name of a country by ShortCode### Get a full SVG The `GetFlagByCountryCode` returns only the content of the SVG file. What it doesn’t have is the `XML` declaration and the `DOCTYPE` declaration. If you want to get the full SVG file, you can use the `GetFullSVG` function in the `SVGFlags` static class. ``` var svgFlag = SVGFlags.GetFullSVG(SVGFlags.lgbt_1, "300", "300"); ``` **Categories:** .NET8, C# **Tags:** country, flags, library --- ### [Using ChatGPT library for grammar checker API](https://puresourcecode.com/dotnet/asp-net/using-chatgpt-library-for-grammar-checker-api/) **Published:** June 18, 2024 **Author:** Enrico **Excerpt:** In this article, we’ll show you a way for using my new ChatGPT library for a grammar checker API with ASP.NET Core step-by-step **Content:** In this article, we’ll show you a way for using my new [ChatGPT library](https://puresourcecode.com/dotnet/net8/new-chatgpt-library-for-c/) for a grammar checker API: the ChatGPT API takes a paragraph and corrects it for grammar and spelling and return it to the user. In other words, we are creating an API inside an API. Here are a few links about other my posts related to ChatGPT: - [New ChatGPT library for C#](https://puresourcecode.com/dotnet/net8/new-chatgpt-library-for-c/) - [Write a ChatGPT client](https://puresourcecode.com/dotnet/csharp/write-a-chatgpt-client/) - [ChatGPT library GitHub repository](https://github.com/erossini/ChatGPTLibrary) - [Source code of this post](https://github.com/erossini/ChatGPTGrammarApi) ## Project Setup First, to get the best experience creating our web application, we’ll want to use the [Visual Studio IDE](https://visualstudio.microsoft.com/downloads/), which you can download and install here. Once it is installed, open Visual Studio and choose a new ASP.NET Core Web API. ![Create a new project with Visual Studio - Using ChatGPT library for grammar checker API](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-6.png?resize=640%2C426&ssl=1)Create a new project with Visual Studio ## Add the new `GrammarFixerController` Now, this will generate a dummy project that can be used to get weather information. We will change this for our purposes. Delete the `WeatherForecastController` file and the new controller called `GrammarFixerController`. The code for the shell controller is shown below. ``` using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace ChatGPTGrammarApi.Controllers { [ApiController] [Route("[controller]")] public class GrammarFixerController : ControllerBase { private readonly ILogger _logger; private IConfiguration _configuration; public GrammarFixerController(ILogger logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; } } } ``` ### Set the version Now let’s add a simple Version endpoint to make sure everything is working okay. We are going to place our Version inside our `appsettings.json` file so we can alter it easily. ``` { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "VERSION": "1.0" } ``` ### Create the API for the version Let’s retrieve the version from appsettings.json using our API. We’ll create a new HTTP GET request to pull the version: ``` using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace ChatGPTGrammarApi.Controllers { [ApiController] [Route("[controller]")] public class GrammarFixerController : ControllerBase { private readonly ILogger _logger; private IConfiguration _configuration; public GrammarFixerController(ILogger logger, IConfiguration configuration) { _logger = logger; _configuration = configuration; } /// /// Versions this instance. /// /// System.String. [HttpGet("version")] public string Version() { return _configuration["VERSION"]; } } } ``` We are ready to run the code! Run the API from the Visual Studio debugger by hitting **F5**. This should pull up the swagger endpoint running on localhost in the browser as shown below: ![ChatGPT Custom API - Version - Using ChatGPT library for grammar checker API](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-7.png?resize=640%2C509&ssl=1)ChatGPT Custom API – Version We can run the version endpoint by clicking the **Try it out** button and by clicking **Execute**. ## Adding our ChatGPT Endpoint Now comes the fun part, adding our ChatGPT Endpoint to take a raw sentence and return the corrected text. In order to access ChatGPT you’ll need to obtain an openai key from openai. Instructions to do that can be found on the [OpenAI page](https://platform.openai.com/account/api-keys). If you need some details on how to do it, please see my [other post](https://puresourcecode.com/dotnet/csharp/write-a-chatgpt-client/). Once you’ve retrieved your endpoint from OpenAI, place it into your appsettings.json file: ``` { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "VERSION": "1.0", "OPENAI_API_KEY": "" } ``` ### Add the FixGrammar endpoint Now let’s create our endpoint to accept the sentence to correct. First, add the code that will retrieve our API key. ``` [HttpPost("fixGrammar")] public string FixGrammar([FromBody] SentencePayloadRequest request) { // retrieve ai key from configuration var openAiKey = _configuration["OPENAI_API_KEY"]; // add open ai code here return "fixed sentence"; } ``` Now, this method receives as a parameter the `SentencePayloadRequest` defined as the following: ``` public class SentencePayloadRequest { /// /// Gets or sets the raw sentence. /// /// The raw sentence. [JsonPropertyName("rawSentence")] public string? RawSentence { get; set; } } ``` ## Add ChatGPT library for C# We have the AI key, but we still need to be able to get to the ChatGPT API, hopefully in a convenient way. There is a Nuget package that serves just that purpose. Browse for [PSC.CSharp.Library.ChatGPT](https://www.nuget.org/packages/PSC.CSharp.Library.ChatGPT/) in your Nuget manager and install it into your project: ![Add PSC.CSharp.Library.ChatGPT to the project](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-8.png?resize=640%2C344&ssl=1)Add [PSC.CSharp.Library.ChatGPT](https://www.nuget.org/packages/PSC.CSharp.Library.ChatGPT/) to the project First, I define the model for the response called `SentencePayloadResponse` ``` public class SentencePayloadResponse { /// /// Gets or sets the fixed sentence. /// /// The fixed sentence. [JsonPropertyName("fixedSentence")] public string? FixedSentence { get; set; } } ``` Now, we are ready to roll. Let’s complete the method to utilize the power of ChatGPT: ``` [HttpPost("fixGrammar")] public async Task FixGrammar([FromBody] SentencePayloadRequest request) { // retrieve ai key from configuration var openAiKey = _configuration["OPENAI_API_KEY"]; if (openAiKey == null) return NotFound("key not found"); var openai = new ChatGpt(openAiKey); var fixedSentence = await openai.Ask( $"Fix the following sentence for spelling and grammar: {request.RawSentence}"); if (fixedSentence == null) return NotFound("Unable to call ChatGPT."); return Ok(new SentencePayloadResponse() { FixedSentence = fixedSentence }); } ``` This method gets the API key from the configuration in appsettings.json and uses it to construct the ChatGpt object from the library. It then uses that object to call **Ask** which sends the prompt to ChatGPT to predict. The prompt is important and instructs chatGPT to fix both grammar and spelling. **Ask** is an asynchronous method, so we need to make the entire FixGrammar endpoint asynchronous to take advantage of this behaviour. In calling Ask, we **await** the response from ChatGPT and once it has finished its processing, we return the results to the user. Below is an example of passing a raw sentence payload and retrieving the response inside Swagger. ![Example of how to use the FixGrammar API](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-9.png?resize=640%2C478&ssl=1)Example of how to use the FixGrammar API ## Wrap up So, in this post, I have just shown you a simple API using my new ChatGPT library for C# to check and fix the grammar of a sentence. If you have more ideas or want to extend this example, please do it and send me your code. Happy coding! **Categories:** .NET8, ASP.NET **Tags:** aspnet-core, chatgpt, webapi **Hashtags:** chatgpt, webapi --- ### [New ChatGPT library for C#](https://puresourcecode.com/dotnet/csharp/new-chatgpt-library-for-c/) **Published:** June 17, 2024 **Author:** Enrico **Excerpt:** I release a new ChatGPT library for C# that helps with the communication and calls to the popular Open AI tool. **Content:** Today, I release a new ChatGPT library for C# that helps with the communication and calls to the popular Open AI tool. One year ago, I showed how to [use ChatGPT in a C# project](https://puresourcecode.com/dotnet/csharp/write-a-chatgpt-client) and keep the conversation going. In this post, also, I explained how to obtain the API Key to use in your application. ## Quick start This is another C# library for [ChatGPT](https://openai.com/chatgpt) using official OpenAI API that allows developers to access ChatGPT, a chat-based large language model. With this API, developers can send queries to ChatGPT and receive responses in real-time, making it easy to integrate ChatGPT into their own applications. ``` using PSC.CSharp.Library.ChatGPT; // ChatGPT Official API var chat = new ChatGpt(""); var response = await chat.Ask("What is the weather like today?"); Console.WriteLine(response); ``` ## Features - Easy to use. - Using official OpenAI API. - Supports both free and pro accounts. - Supports multiple accounts, and multiple conversations. - Support response streaming, so you can get a response while the model is still generating it. ## Getting Started To install `PSC.CSharp.Library.ChatGPT`, run the following command in the Package Manager Console: ``` Install-Package PSC.CSharp.Library.ChatGPT ``` Alternatively, you can install it using the .NET Core command-line interface: ``` dotnet add package PSC.CSharp.Library.ChatGPT ``` ## Usage ### ChatGPT Official API Here is a sample code showing how to use `PSC.CSharp.Library.ChatGPT`: ``` using PSC.CSharp.Library.ChatGPT; // ChatGPT Official API var bot = new ChatGpt(""); // get response var response = await bot.Ask("What is the weather like today?"); Console.WriteLine(response); // stream response await bot.AskStream(response => { Console.WriteLine(response); }, "What is the weather like today?"); // get response for a specific conversation var response = await bot.Ask("What is the weather like today?", "conversation name"); Console.WriteLine(response); // stream response for a specific conversation await bot.AskStream(response => { Console.WriteLine(response); }, "What is the weather like today?", "conversation name"); ``` ## Configuration options ### ChatGPT Official API ``` ChatGptOptions { string BaseUrl; // Default: https://api.openai.com double FrequencyPenalty; // Default: 0.0; long MaxTokens; // Default: 64; string Model; // Default: gpt-3.5-turbo double PresencePenalty; // Default: 0.0; string[]? Stop; // Default: null; double Temperature; // Default: 0.9; double TopP; // Default: 1.0; } ``` #### BaseUrl By default, the base URL is `https://api.openai.com`. You can set the `BaseUrl` to a free reverse proxy server to use ChatGPT Official API for free. #### Frequency Penalty How much to penalize new tokens based on their existing frequency in the text so far. Decreases the model’s likelihood to repeat the same line verbatim. #### Max Tokens The maximum number of tokens to **generate** is shared between the prompt and completion. The exact limit varies by model. (One token is roughly 4 characters for standard English text) #### Model The model to use. The default is `gpt-3.5-turbo`. The `ModelValue` class contains the popular models to use. - gpt-3.5-turbo - gpt-3.5-turbo-16k - gpt-4-turbo You can also set the `Model` with any other model name. For more details, see the [OpenAI API Model documentation](https://platform.openai.com/docs/models). #### Presence Penalty How much to penalize new tokens based on whether they appear in the text so far. Increases the model’s likelihood to talk about new topics. #### Stop Up to four sequences where the API will stop generating further tokens. The returned text will not contain the stop sequence. #### Temperature Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. #### TopP Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. ## Examples ### ChatGPT Console App This is a simple console app that uses `PSC.CSharp.Library.ChatGPT` to interact with ChatGPT. ``` using PSC.CSharp.Library.ChatGPT; // ChatGPT Official API var chat = new ChatGpt(""); var prompt = string.Empty; while (true) { Console.Write("You: "); prompt = Console.ReadLine(); if (prompt is null) break; if (string.IsNullOrWhiteSpace(prompt)) break; if (prompt == "exit") break; Console.Write("ChatGPT: "); await chat.AskStream(Console.Write, prompt, "default"); Console.WriteLine(); } ``` ### Use a different model You can use a different model by passing the model name to the constructor. ``` var bot = new ChatGpt("", new ChatGptOptions { Model = "text-davinci" }); ``` ### Using ChatGPT Official API For Free you can use ChatGPT Official API by setting the base URL to a free reverse proxy server. ``` var bot = new ChatGpt("", new ChatGptOptions { BaseUrl = "https://api.youreverseproxy.com" }); ``` ## Errors When a call to ChatGPT fails, there are several possible errors that can be returned. The full error will be in the `Error` property. A common example is ``` { "choices": null, "created": 0, "error": { "message": "You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.", "type": "insufficient_quota", "param": null, "code": "insufficient_quota" }, "id": null, "model": null, "object": null, "usage": null, "success": false } ``` ### insufficient\_quota This is a very common error. That means you haven’t add a payment details on your account, you haven’t add money in the account or you have reached the limit of your quote. **Categories:** .NET8, C# **Tags:** chatgpt, library, openai **Hashtags:** chatgpt, openai --- ### [MAUI raises on iOS MT1045](https://puresourcecode.com/dotnet/maui/maui-raises-on-ios-mt1045/) **Published:** June 3, 2024 **Author:** Enrico **Excerpt:** When from Visual Studio you deploy a real device a MAUI application raises on iOS the error MT1045. Here what to check to fix the problem **Content:** I wrote already a post about how to [deploy a MAUI application on a real device](https://puresourcecode.com/tools/visual-studio-tools/deploy-maui-apps-on-a-real-device/) but, recently, very often the process raises on iOS the error MT1045. The error I get is this one ``` error MT1045: Failed to execute 'devicectl': 'devicectl -j /var/folders/dm/bwmxpbzn6bvdsyy73c_b453w0000gn/T/tmpoVlvWe.tmp device install app --device "Enrico???s Test iPhone" /Users/enrico/Library/Caches/Xamarin/mtbs/builds/LanguageInUse/1fa03704bb15e35c6f47a701d9d92131e3e0740198296a93338bb3c829bc9cf7/bin/Debug/net8.0-ios/ios-arm64/device-builds/iphone13.1-17.4.1/LanguageInUse.app' returned the exit code 1. ``` So, I have my app in Visual Studio, I connect my iMac to it and I deploy the application on the real device. What you can see is that the app is started to be deploy, you can see the progress but suddenly, the installation returns an error. In the following video you have an example. ## Start the checks First, I have to check if the device is in **Developer mode**. I’m pretty sure it is because I deploy on it already but just in case. 1. **Developer mode** is not enabled on the iOS device. See [Apple – Enable Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device). 2. There is another app installed with the same bundle identifier. To solve this, delete the conflicting app or change your app’s bundle identifier. 3. Certificate/provisioning profile related issues. 4. If you don’t trust the phone from your Mac, you can also try this step. If so, please ignore this: Open Mac -> **Xcode** -> **Settings** -> **Devices and Simulators**. Xcode will pair your device. ## Download Profiles Now, every time you change the configuration of your app in the Apple Developer Portal, you have to download the new certificates for you app. Although I don’t think I have changed anything, I tried to download the Profiles of my Apple account both in Visual Studio and in Xcode. In Visual Studio, open **Tools** > **Options** and then search for the option **Apple Account**. Then, click on you developer account and click **View Details**. A new window will open for the Details of your account. Click on **Download All Profiles**. ![Download iOS certificates from Visual Studio - MAUI raises on iOS MT1045](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/image.png?resize=640%2C399&ssl=1)Download iOS certificates from Visual Studio Similarly, in Xcode. Open Xcode, on the menu click on **Settings**. Under **Accounts**, click on **Download Manual Profiles**. ![Download iOS certificates from Xcode on an iMac - MAUI raises on iOS MT1045](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/Screenshot-2024-05-25-at-18.00.59.png?resize=640%2C451&ssl=1)Downlaod iOS certificates from Xcode ## Turn on Developer Mode 1. On your device, go to **Settings** > **Privacy and Security**. 2. Inside **Privacy and Security** screen, scroll to the bottom to find **Security** section and **Developer Mode**. 3. Tap on **Developer Mode**. 4. Inside **Developer Mode** screen, turn on *Developer Mode*. 5. When prompted with “When Developer Mode is turned on, your device security will be reduced. Restart your device to turn on Developer Mode, tap on “**Restart**”. 6. Once restarted, you’ll get another prompt, “Turn on Developer Mode? Developer Mode allows you to use features that are required for app development. When Developer Mode is turned on, your device security will be reduced.”, tap “Turn On” to turn on Developer Mode on your device. ![Turn on Developer Mode on iOS - MAUI raises on iOS MT1045](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/IMG_0005.png?resize=473%2C1024&ssl=1)Enable Developer Mode on iOS ![Turn on developer mode on iOS- MAUI raises on iOS MT1045](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/IMG_0006.png?resize=473%2C1024&ssl=1)Enabled Developer Mode on iOS Still not working? ## My iPhone on Xcode Next, I want to verify if my device is paired with Xcode. The Xcode version I use is 15.4. ![Xcode version - MAUI raises on iOS MT1045](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/Screenshot-2024-05-25-at-18.20.15.png?resize=640%2C330&ssl=1)Xcode version In order to pair or see the list of paired devices, open the menu **Window** and then **Devices and Simulators**. In this window, you have the status of all devices. If the pair is in progress, you see a yellow band on the top of the window. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/xcode-devices.png?resize=640%2C456&ssl=1)Pair devices on Xcode ## How to find the issue? The app is still not working. Next step is to investigate more about the reason. From the window **Devices and Simulators**, for each device you can open the **Console** and filter for the name of your app, for example. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/xcode-devices.png?resize=640%2C456&ssl=1)Devices and Simulators on Xcode Now, the **Console** for your device, show you all the logs on the device for everything. You can stop the flush of data and filter them. For example, this is the screen I get. If you click on a row, you see all the details in the window below. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/05/ios-console.png?resize=640%2C435&ssl=1)Using this tool, I can see the following error: ``` -[IXSDataPromise cancelForReason:client:error:]: : canceled by client 17 for reason Error Domain=IXUserPresentableErrorDomain Code=14 "This app was not installed because its integrity could not be verified." UserInfo={NSUnderlyingError=0x4f82b0030 {Error Domain=MIInstallerErrorDomain Code=13 "Failed to verify code signature of /var/installd/Library/Caches/com.apple.mobile.installd.staging/temp.jm7AyD/extracted/LanguageInUse.app : 0xe8008015 (A valid provisioning profile for this executable was not found.)" UserInfo={NSLocalizedDescription=Failed to verify code signature of /var/installd/Library/Caches/com.apple.mobile.installd.staging/temp.jm7AyD/extracted/LanguageInUse.app : 0xe8008015 (A valid provisioning profile for this executable was not found.), LegacyErrorString=ApplicationVerificationFailed, SourceFileL ``` This error is generated for different reasons. First, check if the device is registered on the [Apple Developer](https://developer.apple.com/) website. ## Register the device on Apple Developer website My solution was to go to [developer.apple.com](https://developer.apple.com/) > **Devices** and add my devices using **Device ID (UDID)**. This was obtained by plugging device into a mac, selecting in finder, and clicking the top device banner to copy the UDID. Then, I had to create a new ad hoc profile and add the devices to the profile. Finally, I had to download and select the new profile in **Targets** > **Signing and Capabilities**. ## Push notifications If your app is using the push notifications, remember that in the **Entitlements.plist** there is a key. If you try to deploy on your device the app but the channel is `Production`, the MAUI deployment raises on iOS the error MT1045. So, remember to change those lines in the **Entitlements.plist**. When you test the application, those lines are ``` aps-environment development ``` when you deploy the application on the Apple Store, you have to change them: ``` aps-environment production ``` ## Delete certificates and profiles For the outdated Profiles, you can skip it via [manual provisioning](https://learn.microsoft.com/en-us/dotnet/maui/ios/device-provisioning/manual-provisioning?view=net-maui-8.0&tabs=vs) or you can delete it, in Windows is under (C:\\\\Users\\{username}\\\\AppData\\\\Local\\\\Xamarin\\\\iOS\\\\Provisioning or Certificates), Mac is under ~/Library/MobileDevice/Provisioning Profiles/. You can delete it and then restart the VS and then pair it to Mac again. ## Trouble with NET8 Visual Studio 2022, Visual Studio for Mac and Visual Studio Code make me crazy when I have to deploy apps for iOS (and macOS). I have my `.NET8` `MAUI` application that is working on Windows and Android. I can deploy the application to an iPhone Simulator and a real device. After the upgrade of Visual Studio on my Windows to the version 17.10.1, I can’t build a release of the app to deploy it on a real device or create the package for the Apple Store. So, I tried to build the application on Visual Studio for Mac or Visual Studio Code. In both cases I get this error > .NET 8.0 SDK is required to build this application, and is not installed. Install the latest update to the .NET 8.0 SDK by visiting I installed I don’t know how many times the NET8 package but there is no way to get this message out. So, I open again Visual Studio for Mac and I found that I have to enable the option for NET8 under Preview Features. ![Visual Studio for Mac - Preview features](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-2.png?resize=640%2C464&ssl=1)If I try to run the project, I get this error > error NETSDK1045: The current .NET SDK does not support targeting .NET 8.0. Either target .NET 7.0 or lower, or use a version of the .NET SDK that supports .NET 8.0. There are months I’m fighting with the creation of iOS apps. It is not very stable. One day is working, another is not. The only think I’m doing is changing the configuration between `development` and `production`. ## Wrap up In conclusion, this is what I checked when MAUI raises on iOS MT1045 during the development and testing process on my local devices. If you have more checks or advice, please text me or add a comment and I will add your suggestions. **Categories:** .NET8, MAUI **Tags:** ios, maui, visual-studio, visualstudio-2022 **Hashtags:** visual-studio --- ### [Language In Use is here!](https://puresourcecode.com/news/language-in-use-is-here/) **Published:** June 13, 2024 **Author:** Enrico **Excerpt:** Language In Use is here! I'm so excited about the presentation of my new project. My new app helps you to learn and improve your language **Content:** Language In Use is here! I’m so excited about the presentation of my new project [Language In Use](https://languageinuse.com). today at [Birkbeck, University of London](https://www.bbk.ac.uk/) with the team of [Careers & Enterprise](https://www.linkedin.com/feed/update/urn:li:activity:7206869712683593728/#) related to the **Pioneer**. ## What is Language In Use? When we start to learn a new **\#language**, the first common advice from any language teacher is to have a notebook where to write all the new words and review them periodically. Also, reading books, articles or watching videos helps you to memorize the structure of the language. But there are some issues. When you look for a word in your notebook, it is not easy to find what you are looking for, swiping all the pages. When you go to a bookshop or search on the Internet, it is difficult to spot content that interests you and at your language level. This is why I’m creating **\#LanguageInUse**. Language In Use is an eco-system powered by artificial intelligence, formed by a website, an app and a community that gives you all the tools you need to learn and improve your language skills. The **\#community** supports you and you can support the community by sharing your **\#dictionaries** and **\#notes**; creating groups with your mates and studying together; tracking your progress; joining the groups from your school and university where you find appropriate content created by them that download, study and practice; talking with our tireless AI teachers. So, I love to hear your feedback. Please download the app for **\#iOS**, **\#Android** and **\#Windows** and visit the website. ## Links - [LanguageInUse website](https://languageinuse.com/) - [Windows application](https://apps.microsoft.com/detail/LanguageInUse/9NRCGZ1QZB0Q?launch=true&mode=mini) - [Google Play](https://play.google.com/store/apps/details?id=com.languageinuse.app) - [Apple Store for iOS](https://apps.apple.com/app/language-in-use/id6477356057) (iPhone and iPad) - Apple Store for macOS (soon) ## Technologies Now, the project is created by myself only with **\#NET8** and C# using **\#MAUI** for the apps and **\#Blazor** for the website. The infrastructure is on **\#Azure**. I’m building it to offer free tools to people who want to learn languages. New functionalities weekly. [Language In Use](https://languageinuse.com/) is a quite complex project. I wanted to use it as my personal experiment to learn new technologies and improve my skills. As a long-time developer, I have seen many technologies come and go. I have seen many frameworks and libraries that promised to be the best and the most used. I have seen many developers giving up on them. So, my decision was to use the most stable and reliable technologies that I know: those technologies are coming from Microsoft and the framework is called [NET](https://dotnet.microsoft.com/). ### Latest version The latest version of .NET is **.NET 8** released in November 2023. This is a software development platform that provides a collection of technologies for building apps for Linux, macOS, Windows, iOS, Android, and more. .NET 8 is the successor to .NET 7 and will be supported for three years as a long-term support (LTS) release. It includes many new features and improvements, such as performance boosts, ASP.NET Core enhancements, and .NET MAUI platform and stability improvements. ## Language In Use website (LIU) LIU is a web application that uses the latest version of .NET, **.NET 8** and [C#](https://puresourcecode.com/?category=csharp&s=). It is an [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) application that uses [Blazor](https://blazor.net/) as a framework for the UI. The application is hosted on [Microsoft Azure](https://www.puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud) and uses [Azure App Service](https://azure.microsoft.com/en-us/services/app-service/) as a service. ## Language In Use backend The backend is an [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) application built with C# that uses [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) as an ORM (Object Relational Mapper) to access the database. The database is a [Microsoft SQL Server](https://www.microsoft.com/en-us/sql-server/sql-server-2019) database. ## Language In Use Desktop & Mobile application The desktop and mobile application is an [MAUI](https://puresourcecode.com/?s=maui) application built in C#. MAUI is great! With one single project, you can build applications for Windows, Linux, macOS, iOS, Android, and more. **Categories:** .NET8, Blazor, MAUI, News **Tags:** blazor, maui, net8 **Hashtags:** blazor, maui, net8 --- ### [Maui CommunityToolkit Popup crashes applications](https://puresourcecode.com/dotnet/maui/maui-communitytoolkit-popup-crashes-applications/) **Published:** June 11, 2024 **Author:** Enrico **Excerpt:** I use the MAUI CommunityToolkit version 9.0.1 and in particular Popup crashes my applications. I discovered why and how to avoid it **Content:** In my `.NET8` `MAUI` application, I use the `MAUI CommunityToolkit` version 9.0.1 and in particular Popup crashes my applications. I saw a [bug on GitHub](https://github.com/CommunityToolkit/Maui/issues/1844) that is exactly my issue but there is no answer for it. Here what I understand about this error and how to avoid it. ## Scenario Following my post [Open a loading popup from MAUI ViewModel](https://puresourcecode.com/dotnet/maui/open-a-loading-popup-from-maui-viewmodel), I defined my Popup called `LoadingPopup` view in my new project. This is the full code of the popup. ``` ``` ### Open the popup from the ViewModel Now, I want to display the popup from the ViewModel associate to a page. For reference, I’m going to call this page the `MainPage`. So, in the constructor of the page I bind the ViewModel. ``` public DictionaryWordsList(DictionaryWordsListViewModel model) { InitializeComponent(); vm = model; BindingContext = vm; } ``` Then, in the ViewModel, I have a function that reads some data from the database and the APIs. For this reason, I want to display a nice message to the user that he has to wait, a loading popup basically. This is the code to display the popup. ``` var loadingPopup = new LoadingPopup(); loadingPopup.SetMessage(AppResources.DictionaryWordsLoading); loadingPopup.Opened += async (s, e) => { try { // my code } catch (Exception ex) { } finally { if (loadingPopup != null) await loadingPopup.CloseAsync(); } }; await Application.Current.MainPage.ShowPopupAsync(loadingPopup); loadingPopup = null; ``` Now, I tested the page and it is working fine. ### Add navigation The next step is to add the navigation between the `MainPage` and another page. So, the user can click a button in the UI and is redirected to another page with this code ``` await Shell.Current.GoToAsync($"{nameof(WordEdit)}?DictionaryID={DictionaryID}", true); ``` From this page, the user can go back to the `MainPage`. There is another button when I use this code to redirect the user back ``` await Shell.Current.GoToAsync("..", true); ``` Now, the navigation is sorted. I tested it on iOS, Android and Windows from my machine in `Debug` and it is working. ### Deploy an Apple Store Finally, I packaged the application for the Apple Store and published it. The application is live! Checking the [AppCenter](https://appcenter.ms/), I started to see a lot of error, these kinds of errors ``` libsystem_kernel.dylib 0x1d8ecf000 + 50220 libsystem_c.dylib 0x197d09000 + 482208 LanguageInUse 0x10245c000 + 3312336 LanguageInUse 0x10245c000 + 4931284 LanguageInUse 0x10245c000 + 4931396 LanguageInUse 0x10245c000 + 4931444 LanguageInUse 0x10245c000 + 4632768 LanguageInUse 0x10245c000 + 4641936 LanguageInUse 0x10245c000 + 5814296 libsystem_pthread.dylib 0x1ecc73000 + 24684 ``` ![iOS Error in AppCenter - Maui CommunityToolkit Popup crashes applications](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-3.png?resize=640%2C488&ssl=1)iOS Error in AppCenter For me, those errors are impossible to understand. How you can see, I got a lot of those kind of errors. Now, I have to find a way to get some logs and understand what it happens. ![Example of the list of errors in AppCenter - Maui CommunityToolkit Popup crashes applications](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-4.png?resize=640%2C488&ssl=1)Example of the list of errors in AppCenter ## Add MetroLog So, I was looking around to find a way to get logs from my MAUI applications and get more details about the issues. Having logs is always useful and I want to find a solution that I can use everywhere. After a bit of research, I found [MetroLog](https://github.com/roubachof/MetroLog). This is a component for MAUI that helps you to organize and share your logs. It is quite easy to add to a project and gives you the option to share the logs via email. This is so cool! I won’t explain now how to add MetroLog in detail but in the GitHub page, you have the explanation. The documentation is very clear and the implementation is quite straightforward. ### Release the app again Finally, I managed to publish the app again in every store with the logs. I quite like the idea that the users can send me their logs and analyze them to understand the issues in the apps. So, what I discovered is that the component `Popup` crashes my application only in iOS. The error is this one > CommunityToolkit.Maui.Core.Views.MauiPopup.SetShadowView(UIView& > target) > CommunityToolkit.Maui.Core.Views.MauiPopup.ViewDidLayoutSubviews() > ObjCRuntime.Runtime.ThrowException(IntPtr ) > UIKit.UIApplication.UIApplicationMain(Int32 , String\[\] , IntPtr , > IntPtr ) UIKit.UIApplication.Main(String\[\] , Type , Type ) > LanguageInUse.Program.Main(String\[\] args) ![CommunityToolkit.Maui.Core.Views.MauiPopup.SetShadowView error](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/06/image-5.png?resize=640%2C545&ssl=1)CommunityToolkit.Maui.Core.Views.MauiPopup.SetShadowView error ## The discovery After long hours, I discovered where the problem was. A quick recap. The user is in the `MainPage`, click a button and he is redirected to the second page. From the second page, click to go back. When the application goes back to the `MainPage`, the ViewModel starts again and calls the loading popup code to display the nice message to the user. The issue is that at this point the page is not fully displayed, yet the ViewModel is working here. Because the page is not fully displayed, the `PopUp` component is not on the screen. When it tries to set the shadow of the view, iOS complains (only iOS, Android and Windows don’t care). ## My solution So, what is the solution? My solution is not to display the popup on the page but to replace it with a nice `ActivityIndicator`. Yes, I know this is not the best solution because I want to see something better, but for now this is a good compromise. Please let me know if you have a real solution. For now, this post about *Maui CommunityToolkit Popup crashes applications* wants to give you my thoughts and my discovery. Maybe someone else has the same issue; you are not alone! PS: the feature image of this post is created by Microsoft Designer. I’m not sure how it came up with this image. Any thoughts? 😜 Happy coding! **Categories:** .NET8, MAUI **Tags:** communitytoolkit, maui **Hashtags:** communitytoolkit, maui --- ### [Microsoft unveils All-Digital Xbox Series](https://puresourcecode.com/news/microsoft-unveils-all-digital-xbox-series/) **Published:** June 9, 2024 **Author:** Enrico **Excerpt:** In the Xbox Games Showcase, Microsoft unveils a new all-digital Xbox Series X and revised Series S. Here are all the details of today **Content:** Today, in the [Xbox Games Showcase](https://www.xbox.com/en-GB/events/xbox-games-showcase), Microsoft unveils a new all-digital Xbox Series X and revised Series S. With rumours that Sony is gearing up for the PlayStation 5 Pro, it’s no surprise that Microsoft was hot on its heels with its own mid-gen refresh. Following on from the 2020 release of the Xbox Series X and S, Microsoft has just unveiled its new lineup of consoles. With a new all-white, all-digital Xbox Series X, a 1TB Series S, and a new 2TB special edition Xbox Series X, there’s plenty of choice in the lineup now, with the revisions arriving in Holiday 2024. While we’re still waiting for launch pricing and a firm date, the consoles appear identical in specs for what’s already available aside from the increased storage. In an accompanying [Xbox Wire blog post](https://news.xbox.com/en-us/2024/06/09/xbox-series-x-s-new-console-options/), Microsoft says “With the launch of new games for Xbox Series X|S and PC like Avowed, Indiana Jones and the Great Circle, and Senua’s Saga: Hellblade II, this is one of the best years ever to upgrade your console or jump into Xbox for the first time.” This new console comes after Xbox President Sarah Bond promised the Green Team wasn’t bowing out of the physical gaming space, reiterating: “We’re also invested in the next generation roadmap and what we’re really focused on there is delivering the largest technical leap you’ve ever seen in a hardware generation.” There had been rumblings of Microsoft announcing a portable Xbox console to rival the handheld capabilities of the Switch and Steam deck. After all, who wouldn’t want to play The Elder Scrolls 6 on the move? But, as yet, there appears to be no incoming handheld on the horizon. This is a crucial time for Microsoft, with the massive acquisitions of ZeniMax Media and Activision Blizzard King meaning the gaming giant now holds the keys to everything from The Elder Scrolls to Fallout, Overwatch to Call of Duty. The latter is the big hitter here, with the CoDverse getting a lick of green paint in 2024. **Categories:** Microsoft, News **Tags:** games, xbox, xbox-series-s **Hashtags:** microsoft, xbox --- ### [New Harry Potter game is coming](https://puresourcecode.com/news/new-harry-potter-game-is-coming/) **Published:** June 7, 2024 **Author:** Enrico **Excerpt:** Today at Summer Game Fest, Warner Bros. Games has revealed that Harry Potter: Quidditch Champions is releasing on 3rd September **Content:** Today at Summer Game Fest, Warner Bros. Games has revealed that **Harry Potter: Quidditch Champions** is releasing on 3rd September. Coming to PS4, PS5, Xbox One, Xbox Series X/S, Switch and PC, it’ll also be launching day one as a free PS Plus Game for all subscribers. After the success of [Hogwarts Legacy](https://puresourcecode.com/games/hogwarts-legacy-is-a-successful-game/), Harry Potter: Quidditch Champions lets players enjoy a thrilling game of quidditch either solo or online — with and against friends. Filled with familiar faces from the Wizarding World and plenty of references, it aims to be a fun and engaging experience for all Harry Potter fans. Unlike Hogwarts Legacy, Harry Potter: Quidditch Champions seems to be set at the same time as the book and film series, with characters such as Ron Weasley, Hermione Granger and even Harry Potter himself making an appearance in the trailer. **Categories:** News **Tags:** harry-potter, hogwarts-legacy **Hashtags:** harry-potter, quidditch --- ### [MS-DOS 4.0 is now open source](https://puresourcecode.com/news/ms-dos-4-0-is-now-open-source/) **Published:** April 29, 2024 **Author:** Enrico **Excerpt:** From today, Microsoft released on GitHub MS-DOS 4.0. MS-DOS 4.0 is now open source and available with the other older versions of the OS. **Content:** Ten years ago, [](https://devblogs.microsoft.com/commandline/re-open-sourcing-ms-dos-1-25-and-2-0/)[Microsoft released the source for MS-DOS 1.25 and 2.0](https://devblogs.microsoft.com/commandline/re-open-sourcing-ms-dos-1-25-and-2-0/) to the Computer History Museum, and then [later republished them](https://github.com/microsoft/MS-DOS) for reference purposes. This code holds an important place in history and is a fascinating read of an operating system that was written entirely in 8086 assembly code nearly 45 years ago. The source code is available on [GitHub](https://github.com/microsoft/MS-DOS) with the other oldest versions of the operating system. ![MS-DOS 4.0](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/04/image-21.png?resize=250%2C250&ssl=1)MS-DOS 4.0 Today, in partnership with IBM and in the spirit of open innovation, we’re releasing the source code to MS-DOS 4.00 under the MIT license. There’s a somewhat complex and fascinating history behind the 4.0 versions of DOS, as Microsoft partnered with IBM for portions of the code but also created a branch of DOS called Multitasking DOS that did not see a wide release. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/04/image-20.png?resize=640%2C437&ssl=1) A young English researcher named [Connor “Starfrost” Hyde](https://starfrost.net/blog/001-mdos4-part-1/) recently corresponded with former Microsoft Chief Technical Officer Ray Ozzie about some of the software in his collection. Amongst the floppies, Ray found unreleased beta binaries of DOS 4.0 that he was sent while he was at Lotus. Starfrost reached out to the Microsoft Open Source Programs Office (OSPO) to explore releasing DOS 4 source, as he is working on documenting the relationship between DOS 4, MT-DOS, and what would eventually become OS/2. Some later versions of these Multitasking DOS binaries can be found around the internet, but these new Ozzie beta binaries appear to be much earlier, unreleased, and also include the ibmbio.com source. Scott Hanselman, with the help of internet archivist and enthusiast Jeff Sponaugle, has imaged these original disks and carefully scanned the original printed documents from this “Ozzie Drop”. Microsoft, along with our friends at IBM, think this is a fascinating piece of operating system history worth sharing. Jeff Wilcox and OSPO went to the Microsoft Archives, and while they were unable to find the full source code for MT-DOS, they did find MS DOS 4.00, which we’re releasing today, alongside these additional beta binaries, PDFs of the documentation, and disk images. We will continue to explore the archives and may update this release if more is discovered. Thank you to Ray Ozzie, Starfrost, Jeff Sponaugle, Larry Osterman, Mark Zbikowski, our friends at the IBM OSPO, as well as the makers of such digital archeology software including, but not limited to Greaseweazle, Fluxengine, Aaru Data Preservation Suite, and the HxC Floppy Emulator. Above all, thank you to the original authors of this code, some of whom still work at Microsoft and IBM today! If you’d like to run this software yourself and explore, we have successfully run it directly on an original IBM PC XT, a newer Pentium, and within the open source PCem and 86box emulators. **Categories:** Microsoft, News **Tags:** history, microsoft, msdos **Hashtags:** msdos --- ### [MAUI Push Notifications using Azure Notification Hub](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/) **Published:** February 23, 2024 **Author:** Enrico **Excerpt:** Here how implement in NET8 MAUI the push notifications using Azure Notification Hub without any external package for Windows. Very hard work! **Content:** It is more than 2 weeks since I tried to configure and implement in my [NET8](https://puresourcecode.com/?s=net8) [MAUI](https://stackoverflow.com/questions/77930377/net-8-maui-and-azure-notification-hub-configuration-is-not-working) application the push notifications using [Azure Notification Hubs](https://stackoverflow.com/questions/77930377/net-8-maui-and-azure-notification-hub-configuration-is-not-working). Also, I paid the [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/) support (not very useful), and I still can’t configure the hub for Windows and Android. So, I tried another plugin at this point but had to ignore the Windows notification (sigh!). I tried: - [FirebasePushNotificationPlugin](https://github.com/thomasgalliker/Plugin.FirebasePushNotifications) but unfortunately this plugin doesn’t have documentation and has an error at least with `.NET8` and the error is > Could not find a part of the path ‘C:\\Users\\enric.nuget\\packages\\xamarin.firebase.ios.installations\\8.10.0.3\\lib\\net6.0-ios15.4\\Firebase.Installations.resources\\FirebaseInstallations.xcframework\\ios-arm64\_x86\_64-simulator\\FirebaseInstallations.framework\\Headers\\FirebaseInstallations-umbrella.h’. - [Plugin.FirebasePushNotification](https://github.com/CrossGeeks/FirebasePushNotificationPlugin) but unfortunately it doesn’t seem that it works. I added [all the code](https://github.com/CrossGeeks/FirebasePushNotificationPlugin/blob/master/samples/FirebasePushSample.Android/MainApplication.cs) to the Android project without notification from the Firebase portal. - [Plugin.Firebase](https://github.com/TobiasBuchholz/Plugin.Firebase) but also this plugin has an issue with missing files and the error is > Could not find a part of the path ‘C:\\Users\\enric.nuget\\packages\\xamarin.firebase.ios.installations\\8.10.0.3\\lib\\net6.0-ios15.4\\Firebase.Installations.resources\\FirebaseInstallations.xcframework\\ios-arm64\_x86\_64-simulator\\FirebaseInstallations.framework\\Headers\\FirebaseInstallations-umbrella.h’. - I followed the implementation of [Vladislav Antonyuk](https://vladislavantonyuk.github.io/articles/.NET-MAUI-Push-Notifications-using-Azure-Notification-Hub.-Part-2.-Setup-.NET-MAUI/) but at the moment it is not working for me - I read some comments on GitHub where Microsoft said that the push notification is embedded in MAUI but I can’t find any further documentation After a lot – very a lot – of bad words and nights without sleeping, I sorted it out. It is quite complicated because the settings are difficult to understand and the documentation is not really clear. So, I show you everything I discovered without using external plugin but only what MAUI offers and `HttpClient`. I split this topic in a few posts: - [MAUI Push Notifications using Azure Notification Hub](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/) - [MAUI Push Notifications using Azure Notification Hub for Android](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-android/) - [MAUI Push Notifications using Azure Notification Hub for iOS](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-ios/) ## Setting up Azure Notification Hubs First, I have to set up the Azure Notification Hubs and this is a piece of cake, just follow the process in the Azure portal. ### Creation of the Notification Hub So, in order to create a new Azure Notification Hub, navigate to the [Azure portal](https://portal.azure.com), and then create or use a [Resource Group](https://puresourcecode.com/tools/azure-tools/azure-resource-naming-convention-guide/). 1. Click on **Create a resource** 2. In the search box, type **Notification Hub** and click on it. ![- MAUI Push Notifications using Azure Notification Hub](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-11.png?resize=457%2C486&ssl=1) 5. Enter all the necessary details and click **Create**. In my case, and this is important I type those values to use later in the code: - **Notification Hub Namespace**: *languageinuse* - **Notification Hub**: *app* ![- MAUI Push Notifications using Azure Notification Hub](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image.png?resize=560%2C836&ssl=1) ### Important note This is an update after a month I’m running this Notification Hub. I have just received a bill from Microsoft of £368 for a month related to the Notification Hub. ![Azure cost of the Notification Hub](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-20.png?resize=354%2C283&ssl=1)Azure cost of the Notification Hub In the screenshot above, you see the option **Enable availability zones**. Although the price for the Notification Hub is **Free**, this service costs you a lot. ![Azure Notification Hub - Availability zone cost](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-16.png?resize=640%2C203&ssl=1)Azure Notification Hub – Availability zone cost So, if you are doing your tests, uncheck the option to avoid this expensive service. ### Disable the Availability zone After a long search, if you created an **Azure Notification Hub Namespace** checking the **Availability zone**, there is no way to remove it. You have to re-create the namespace. So, check the following screenshot. ![Disable the Availability zone](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-19.png?resize=640%2C525&ssl=1)Disable the Availability zone ### Disable the disaster recovery Now, if you checked the option **Enable availability zones**, probably you want to disable it. For that, open each **Notification Hub** (not the Notification Hub Namespace) and in the **Essentials** section, you find **Flexible recovery region**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-17.png?resize=640%2C130&ssl=1)When you click on the link on the right – in the screenshot Southeast Asia – a blade is opened and the title is **Edit Disaster Recovery**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-18.png?resize=640%2C246&ssl=1)From here, uncheck **Enable disaster recovery** and then click **Save**. ### Configuring the Notification Hub After creation, configure your newly created Notification Hub: 1. Go to your **Notification Hub** and under the **Manage** section, select **Access Policies**. ![- MAUI Push Notifications using Azure Notification Hub](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-14.png?resize=640%2C439&ssl=1) 2. Note down the **DefaultListenSharedAccessSignature** connection strings provided. In particular, the last part of the connection is important: that is your key to use in the code later. ## Setting up Windows Notification Service (WNS) Now, sit back, relax and start to swear. `WNS` delivers notifications from cloud servers to Windows apps running on Windows 8, Windows Phone (sigh, unfortunately it doesn’t exist anymore) and later. My focus is [Windows 11](https://puresourcecode.com/tag/windows11/) and later. #### Find the Package SID for your Windows application 1. Go to the [Windows Dev Center’s Dashboard](https://partner.microsoft.com/en-us/dashboard/home), and sign in with your account. 2. Choose your app from the list of apps. 3. Click on **Product identity**. 4. Find the **Package SID** value. We will need it later when configuring WNS with Azure. The **Package SID** is a very long ID like `S-1-15-2-283421221-……….-……….-………` ![](data:image/png;base64,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) Now, from the [documentation](https://learn.microsoft.com/en-us/previous-versions/windows/apps/hh465407(v=win.10)), Microsoft says to go to the **WNS\\MPNS** section and register from here your application. Unfortunately, this is not valid anymore. Be careful because this is a tricky part. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-2.png?resize=640%2C304&ssl=1)### Add your app in the Azure App Registration First, a little bit of background. I’m using the **Developer Windows** portal and **Azure portal** with the same account. In all the documentation, I found that I have to create a new **App Registration** for my app. The real problem is where creating this new registration. If I create a new registration and this will appear under **Owned applications** tab, when you try to add an app in the Azure Notification Hub, an error occurs: ``` {"error":{"code":"BadRequest","message":"Invalid WNS credentials."}} ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-16.png?resize=640%2C228&ssl=1)All the support I got and the answers to my question was to register the application in the **App Registration**. I tried quite a few times to register the app but I have never seen or been told to create the app under the **Applications from personal account**. ![App Registration in the Azure portal from personal account](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-15.png?resize=640%2C304&ssl=1)App Registration in the Azure portal from personal account So, when in you Microsoft Entra ID, you add a **New registration**, in the prompt **Where should this application be registered?**, you have to select **Only associate with personal account**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-17.png?resize=640%2C240&ssl=1)Then, fill the form and create the new app. At this point, you are ready to continue. Now, clik on your app and follow the steps: 1. Click on **Certificates & secrets** 2. Click on **New client secret** and **create a new secret**. Note down the secret value. It is your **Security Key**. We will need it later when configuring WNS with Azure. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-4.png?resize=640%2C251&ssl=1) ### Configure WNS with Azure 1. Go to the Azure portal. 2. In your **Notification Hub**, under **Settings**, select **Windows (WNS)**. 3. Fill in the **Package SID** and **Security Key** details: - **Package SID**: the value you obtained in the previous step. The format is `ms-app://`. - **Security Key**: the value you obtained in the previous step. 4. Click **Save**. > The format of the Package SID must be ms-app://S-x-xxxx-xxxxxx ## Setting up your .NET MAUI Project for Windows Windows doesn’t require any additional packages. We only need to register our device and configure what to do when the a message arrives. > Only the application is running or running in the background, it will receive the notification from the sever. This is quite important. If you send a push notification to Windows, only if the users have the application open, they will receive your message. If you want to be sure your message will be delivered also when it is close, you have to create a background service. Here [some documentation](https://learn.microsoft.com/en-us/previous-versions/windows/apps/jj709906(v=win.10)). I haven’t tried to create that yet. ### Registering for Push Notifications In your `Platforms/Windows/App.xaml.cs` handle the startup logic to register for push notifications. For Windows, use `PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync()` to create a channel and register it with `NotificationHub`. For that, I’m using the code from [Vladislav Antonyuk](https://vladislavantonyuk.github.io/articles/.NET-MAUI-Push-Notifications-using-Azure-Notification-Hub.-Part-2.-Setup-.NET-MAUI/) with my correction. #### Create a valid token First, we have to be sure that we have a valid token in order to call the API for the Azure Notification Hub to register our device. The reference for that is in the [Microsoft documentation](https://learn.microsoft.com/en-us/previous-versions/azure/reference/mt621153(v=azure.100)). ``` private static string CreateToken(string resourceUri, string keyName, string key) { var sinceEpoch = DateTime.UtcNow - DateTime.UnixEpoch; var week = 60 * 60 * 24 * 7; var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week); var stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign))); var sasToken = string.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName); return sasToken; } ``` #### Register your device Then, we have to register our devices to the Azure Notification Hub. This is the full function. ``` private async Task RegisterDevice(string notificationHub, string key) { var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync(); channel.PushNotificationReceived += Channel_PushNotificationReceived; var deviceInstallation = new { InstallationId = new EasClientDeviceInformation().Id, Platform = "wns", PushChannel = channel.Uri }; using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("x-ms-version", "2015-01"); httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", CreateToken($"https://{notificationHubNamespace}.servicebus.windows.net", "DefaultListenSharedAccessSignature", key)); await httpClient.PutAsJsonAsync($"https://{notificationHubNamespace}.servicebus.windows.net/" + $"{notificationHub}/installations/{deviceInstallation.InstallationId}" + "?api-version=2015-01", deviceInstallation); } ``` Remember that `notificationHubNamespace` is different from `notificationHub`. The `notificationHubNamespace` is the name of the Azure Notification Hub (in my case *languageinuse*) and the `notificationHub` is the spefic part of the hub (in my case *app*). Then, I have to add the code to receive the push notification message from the line 4 and doing something with the message (for example show it) ``` private void Channel_PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs args) { var notification = args.RawNotification; // implement your logic here... } ``` At the end, I have to call the function to register the device when the app starts. ``` protected override async void OnLaunched(LaunchActivatedEventArgs args) { base.OnLaunched(args); await RegisterDevice("notificationHubNamespace", "YOUR SharedAccessKey"); } ``` The `SharedAccessKey` is the key I saved from the App Registration before. ## Test your implementation Finally, this is the easy part. Open your hub and click on **Test Send**. Select Windows as a platform and then click on **Send**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-18.png?resize=640%2C338&ssl=1)If everything is correct and the app is open, you will receive the message. Now, you can display it or read the data to do something else. ## Wrap up In conclusion, after almost 3 weeks, I found how to implement in MAUI the push notifications using Azure Notification Hub for Windows. I’m writing a new post about Android that it is working already. For iOS, I need a little bit of support and understand how to do it. Hopefully, soon, I can show you also iOS. **Categories:** .NET8, MAUI **Tags:** azure, azure-notification-hub, maui, net8, windows, windows-apps **Hashtags:** maui, net8 --- ### [Deep linking for NET8 MAUI](https://puresourcecode.com/dotnet/maui/deep-linking-for-net8-maui/) **Published:** March 5, 2024 **Author:** Enrico **Excerpt:** In this post everything you have to do to implement deep linking for NET8 MAUI for mobile and desktop applications **Content:** I am building an app using NET8 MAUI and I want the users have the ability to open a link in email or on a website that would open this mobile app on iOS and Android or a desktop application for Windows and macCatalyst. This is also known as **Deep Linking**. There are 2 different ways to accomplish this: one is at a high level there is a method also known as **Universal Links**. The other way, that I am more used to, is where you use a custom HTTP schema. The high-level difference is that Universal Links use the http:// or https:// format of the URL, whereas the custom schema method uses your schema as in `myschema://`. The downside of using the Universal Link method is that you need to have a website running that can accept incoming requests for a configuration file. I didn’t want that requirement for this particular app, so I decided to use the custom schema method. When I started searching for how to implement this method, I found so many articles that it was overwhelming, in the end, I was able to get it to work, but only after much blog reading, video watching, and trial and error. ## Android First, I have to add the following method to your MainActivity.cs file: ``` protected override void OnCreate(Bundle savedInstanceState) { base.OnCreate(savedInstanceState); var uri = Intent?.Data; if (uri != null) { // here your code } } ``` This will detect that the app was started or resumed using a custom URL schema. The URI value is the URL used to launch the app. The parameters variable will contain any query parameters on the URL that you can use to do specific things. Add the following attribute just above your `MainActivity.cs` file declaration of your MainActivity class: ``` [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.ActionView, Intent.CategoryDefault, Intent.CategoryBrowsable, }, DataScheme = "mycustomschema", DataHost = "", DataPathPrefix = "/")] ``` The **DataSchema** must match what will be the beginning part of the URL used to launch the app. In the above code, the URL would be `mycustomschema://`. The `DataPathPrefix` can require something after the `//` in order to match and launch your app. ## Change AndroidManifest Now, the final step is to change the `AndroidManifest.xml`. In the [Google documentation](https://developer.android.com/training/app-links), I found these settings ``` ``` What is impossible to find is where to place this part of XML. Here is the solution: place the code in the `application` tag like in the following example (I remove all the useless tags and properties) ``` ``` ## Create Your Assets.json for Android Then, I have to add the new schema in the **Google Play Console**. In the **Deep Links** section, you can see a link to add a new schema. ![- Deep linking for NET8 MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-10.png?resize=640%2C226&ssl=1) You need to create a file to be placed on your website to help Google prove that you own the website and you are allowing the app to be launched by visiting that URL. Google refers to your `assets.json` as your digital asset link, which is a public verifiable statement about which page on a website is associated with an app. You place this file in */.well-known/assetlinks.json* on your root domain. **Important:** This must be served over HTTPS. The `assets.json` file looks similar to the one below. ``` [{ "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "com.mycompany.myapp", "sha256_cert_fingerprints": ["16:7E:F1:D1:56:33:06:50:D8:AA:B9:95:F2:43:22:55:16:D0:83:42:1E:1D:B2:A8:8A:04:57:32:3F:CF:44:E5"] } }] ``` ## iOS Now, it is the turn of iOS. A lot of posts are recommended to add the following method override to your `AppDelegate.cs` file: ``` [Export("application:continueUserActivity:restorationHandler:")] public override bool ContinueUserActivity(UIKit.UIApplication application, NSUserActivity userActivity, UIKit.UIApplicationRestorationHandler completionHandler) { if (userActivity != null) { string url = userActivity.WebPageUrl?.ToString(); // use the url to extract any query parameters with values if needed } return true; } ``` This code doesn’t do anything in my implementation but it could be useful in a Universal Link implementation. What is really needed is the following function: ``` [Export("application:openURL:options:")] public override Boolean OpenUrl(UIApplication app, NSUrl url, NSDictionary options) { if (!String.IsNullOrEmpty(url.AbsoluteString) && url.AbsoluteString.Contains("mycustomschema")) { if (url.AbsoluteString.Contains("/addword")) { Shell.Current.GoToAsync($"{nameof(WordEdit)}?{url.Query}", true); } } return true; } ``` With this code, the application receives the call from the custom schema (for example from a website link) and reads the `url`. Then, I can check what page the URL contains and then open it. ### Add the Entitlements.plist Create an `Entitlements.plist` file in the `Platforms/iOS` folder with the following content: ``` com.apple.developer.associated-domains applinks:mycustomschema://example.test.com ``` Lastly, add the following lines to your `info.plist` file: ``` com.apple.developer.associated-domains CustomDomainHere.com com.exampleapp.test CFBundleURLSchemes mycustomschema ``` **NOTE**: The `com.exampleapp.test` `bundle Id` used in this example must be changed to match your app’s actual bundle Id, and the domain used must be changed to match your custom domain. That’s all you have to do. I still can’t believe how much time I spent searching and trying things to end up with such a simple result. Every site or tutorial I found had different parts of the puzzle, forcing me to stitch it together piece by piece. Here it all is in one place- and I hope it will help you to avoid the same headaches I had to go through to get here. ### Associate a site to the app For Apple, we must create a json file named **apple-app-site-association.json** in a root folder in your web app. Add the following code to the file: ``` { "applinks": { "apps": [], "details": [ { "appID": "", "paths": [ "*" ] } ] } } ``` The **appID** is `{teamId}.{your app bundle id}`, to get a `teamId` you will see it in [Apple Developer](https://developer.apple.com/programs/) (login and you will see it in the top right). To get `bundleId`, open your MAUI then double click on your project name see **ApplicationId** element it’s your app bundleId. For example: - teamId = XX92LZLAJL - app bundleId = com.companyname.mobileapp - appId = XX92LZLAJL.com.companyname.mobileapp. ### Add associate domain On click on your app. Then enable **Associated Domains,** then click Save and Download a file. Please note that if you used it before you have to replace that file with the newest. ![Apple Developer portal: Certificates, Identifiers & Profiles - Deep linking for NET8 MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-12.png?resize=640%2C478&ssl=1)Apple Developer portal: Certificates, Identifiers & Profiles Add XML file named **Entitlements.plist** in **Platforms/iOS** folder, and then add the following code in this file like below: ``` com.apple.developer.associated-domains applinks:yourdomain.com ``` Remember that whenever you change any capability in the Apple Developer portal, the provisioning profiles must be recreated. ![Apple Developer portal: modify app capabilities - Deep linking for NET8 MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-13.png?resize=483%2C222&ssl=1)Apple Developer portal: modify app capabilities ## Windows The configuration for Windows seems quite easy. In the `Package.appxmanifest` open **Declarations** add a new **Protocol** selected from the **Available Declarations**. ![Package.appxmanifest declarations - Deep linking for NET8 MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-11.png?resize=640%2C377&ssl=1)Package.appxmanifest declarations Here, in the **Name**, add the schema you want to use to open your application. Then, open the `App.xaml.cs`. In the `OnLaunched`, I have to read the arguments and verify if the call is coming from a protocol call. If yes, I can check the URI and then redirect the application to the right page. ``` protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { base.OnLaunched(args); var actEventArgs = Microsoft.Windows.AppLifecycle.AppInstance.GetCurrent().GetActivatedEventArgs(); if (actEventArgs.Kind == ExtendedActivationKind.Protocol) { var d = actEventArgs.Data as IProtocolActivatedEventArgs; if (d != null) { var uri = d.Uri; var uriString = uri.AbsoluteUri; if (!string.IsNullOrEmpty(uriString) && uriString.Contains("liu")) { if (uriString.Contains("/addword")) { await Shell.Current.GoToAsync($"{nameof(WordEdit)}{uri.Query}", true); } } } } } ``` As you can see, I explicitly reference `Microsoft.UI.Xaml.LaunchActivatedEventArgs` because I want to add in this application a background service to receive [push notifications](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/) also when the application is not open. This is a work in progress. ## Wrap up In this post **Deep linking for NET8 MAUI**, I show what I have done to implement deep linking in my MAUI application ([here you have](https://languageinuse.com) more details about it). I hope this can help someone else in not wasting time to find a solution as I did. Let me know what you think about it in the comment below or the [Forum](https://puresourcecode.com/forum/). If you want to support my work, please consider [sponsoring me on GitHub](https://github.com/sponsors/erossini). **Categories:** .NET8, MAUI **Tags:** deep-linking, maui, net8 **Hashtags:** deep-linking, maui, net8 --- ### [MAUI Push Notifications using Azure Notification Hub for Android](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-android/) **Published:** February 23, 2024 **Author:** Enrico **Excerpt:** Here is how to implement in a MAUI project the Push Notifications using Azure Notification Hub for Android. It works simply fine! **Content:** It is more than 2 weeks since I tried to configure and implement in my [NET8](https://puresourcecode.com/?s=net8) [MAUI](https://puresourcecode.com/?s=maui) application the push notifications using [Azure Notification Hubs](https://stackoverflow.com/questions/77930377/net-8-maui-and-azure-notification-hub-configuration-is-not-working) for Android. Also, I paid the [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/) support (not very useful), and I still can’t configure the hub for Windows and Android. So, I tried another plugin at this point but had to ignore the Windows notification (sigh!). So, I show you everything I discovered without using external plugin but only what MAUI offers and `HttpClient`. I split this topic in a few posts: - [MAUI Push Notifications using Azure Notification Hub for Windows](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/) - [MAUI Push Notifications using Azure Notification Hub for Android](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-android/) - [MAUI Push Notifications using Azure Notification Hub for iOS](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-ios/) In this post, I won’t explain how to configure the Azure Notification Hub; if you need more information, please read my [previous post](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/). ## Setting up Firebase Cloud Messaging (FCM) for Android First, **Firebase Cloud Messaging** enables you to send push notifications to Android devices. So, I have to configure it as the first action to proceed. ### Create a Firebase Project 1. Go to the [Firebase console](https://console.firebase.google.com/). 2. Click on ‘Add project’. 3. Follow the instructions and set up your project. ![Projects in the Firebase console - MAUI Push Notifications using Azure Notification Hub for Android](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-20.png?resize=640%2C337&ssl=1)Projects in the Firebase console ### Obtain Server Key 1. Navigate to **Project settings**. 2. Click on the **Cloud Messaging** tab. You will see **Cloud Messaging API (Legacy) Disabled** message. Click on **Manage API in Google Cloud Console**. You will be redirected to Google Cloud Console. Click on **Enable**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-6.png?resize=640%2C273&ssl=1)4. Back to Firebase and copy your **Server key**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-5.png?resize=640%2C247&ssl=1)### Configure FCM with Azure 1. Go to the Azure portal. 2. In your Notification Hub, under **Settings**, select **Google (GCM/FCM)**. 3. Enter your **Server Key**. 4. Click **Save**. ![Google integration in the Azure Notification Hub](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-21.png?resize=621%2C580&ssl=1) That’s it! You now have Azure Notification Hubs integrated with FCM. ## Setting up your .NET MAUI Project ### Add `google-services.json` Open Firebase Console and select Add Firebase to your Android app. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-7.png?resize=640%2C262&ssl=1)On the Add Firebase to your Android app page, enter an Android package name. It should match the package name of your .NET MAUI application. Select Register app. Select Download google-services.json. Then save the file into a `Platforms\Android` folder. In the properties of the file in your project, mark this file as `GoogleServicesJson` ![Set the file as GoogleServicesJson](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-22.png?resize=311%2C294&ssl=1) ### Add Required Packages ``` ``` ### JAVA0000 If you get an error like this one ``` Error JAVA0000 Error in C:\Users\SushmithaBanoji.nuget\packages\xamarin.androidx.collection.jvm\1.3.0.1\buildTransitive\net6.0-android31.0....\jar\androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class: Type androidx.collection.ArrayMapKt is defined multiple times: C:\Users\SushmithaBanoji.nuget\packages\xamarin.androidx.collection.jvm\1.3.0.1\buildTransitive\net6.0-android31.0....\jar\androidx.collection.collection-jvm.jar:androidx/collection/ArrayMapKt.class, C:\Users\SushmithaBanoji.nuget\packages\xamarin.androidx.collection.ktx\1.2.0.5\buildTransitive\net6.0-android31.0....\jar\androidx.collection.collection-ktx.jar:androidx/collection/ArrayMapKt.class Compilation failed ``` then you have to add another package because there is an issue with one of the Xamarin NuGet package ``` ``` ### Firebase is not initialized Another error you can get when the application is trying to get a token with `FirebaseMessaging.Instance.GetToken()` is > Java.Lang.IllegalStateException: ‘Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.’ ``` Java.Lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first. at Java.Interop.JniEnvironment.StaticMethods.CallStaticObjectMethod(JniObjectReference type, JniMethodInfo method, JniArgumentValue* args) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 21452 at Java.Interop.JniPeerMembers.JniStaticMethods.InvokeObjectMethod(String encodedMember, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniStaticMethods.cs:line 165 at Firebase.Messaging.FirebaseMessaging.get_Instance() in C:\a\_work\1\s\generated\com.google.firebase.firebase-messaging\obj\Release\net7.0-android\generated\src\Firebase.Messaging.FirebaseMessaging.cs:line 106 at MauiPushNotification.Platforms.Android.Notification.DeviceInstallationService.RegisterDevice(String notificationHubNamespace, String notificationHub, String key) in C:\Projects\GitHub\MauiPushNotification\MauiPushNotification\MauiPushNotification\Platforms\Android\Notification\DeviceInstallationService.cs:line 29 --- End of managed Java.Lang.IllegalStateException stack trace --- java.lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first. at com.google.firebase.FirebaseApp.getInstance(FirebaseApp.java:179) at com.google.firebase.messaging.FirebaseMessaging.getInstance(FirebaseMessaging.java:126) at crc648356ffd500c1cdc0.MainActivity.n_onCreate(Native Method) at crc648356ffd500c1cdc0.MainActivity.onCreate(MainActivity.java:39) at android.app.Activity.performCreate(Activity.java:8595) at android.app.Activity.performCreate(Activity.java:8573) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loopOnce(Looper.java:205) at android.os.Looper.loop(Looper.java:294) at android.app.ActivityThread.main(ActivityThread.java:8177) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) --- End of managed Java.Lang.IllegalStateException stack trace --- java.lang.IllegalStateException: Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first. at com.google.firebase.FirebaseApp.getInstance(FirebaseApp.java:179) at com.google.firebase.messaging.FirebaseMessaging.getInstance(FirebaseMessaging.java:126) at crc648356ffd500c1cdc0.MainActivity.n_onCreate(Native Method) at crc648356ffd500c1cdc0.MainActivity.onCreate(MainActivity.java:39) at android.app.Activity.performCreate(Activity.java:8595) at android.app.Activity.performCreate(Activity.java:8573) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loopOnce(Looper.java:205) at android.os.Looper.loop(Looper.java:294) at android.app.ActivityThread.main(ActivityThread.java:8177) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) ``` ![Java.Lang.IllegalStateException: 'Default FirebaseApp is not initialized in this process com.languageinuse.app. Make sure to call FirebaseApp.initializeApp(Context) first.'](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-23.png?resize=430%2C257&ssl=1) ### Add permissions Update AndroidManifest.xml: ``` ``` ### Update MainActivity.cs Remember the connection strings from the Azure Notification Hub setup? You’ll need them now. Search for `DefaultListenSharedAccessSignature` access policy and copy `SharedAccessKey`. ``` protected override async void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); await DeviceInstallationService.RegisterDevice("YOUR HUB NAME", "YOUR SharedAccessKey"); } ``` > Don’t use `DefaultFullSharedAccessSignature` in client applications! ### Create DeviceInstallationService.cs Azure Hotification Hub requires device registration, so it knows what device should receive a notification. ``` public static class DeviceInstallationService { private static bool NotificationsSupported => GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(Application.Context) == ConnectionResult.Success; private static string? GetDeviceId() => Settings.Secure.GetString(Application.Context.ContentResolver, Settings.Secure.AndroidId); public static async Task RegisterDevice(string notificationHubNamespace, string notificationHub, string key) { if (!NotificationsSupported) return; try { var firebaseToken = await FirebaseMessaging.Instance.GetToken(); var deviceInstallation = new { InstallationId = GetDeviceId(), Platform = "gcm", PushChannel = firebaseToken.ToString() }; using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("x-ms-version", "2015-01"); httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", CreateToken($"https://{notificationHubNamespace}.servicebus.windows.net", "DefaultListenSharedAccessSignature", key)); await httpClient.PutAsJsonAsync($"https://{notificationHubNamespace}.servicebus.windows.net/{notificationHub}" + $"/installations/{deviceInstallation.InstallationId}?api-version=2015-01", deviceInstallation); } catch(Exception ex) { LogCenter.Save("[Android] Push Notification Registration Error", "", exc: ex); } } private static string CreateToken(string resourceUri, string keyName, string key) { var sinceEpoch = DateTime.UtcNow - DateTime.UnixEpoch; var week = 60 * 60 * 24 * 7; var expiry = Convert.ToString((int)sinceEpoch.TotalSeconds + week); var stringToSign = HttpUtility.UrlEncode(resourceUri) + "\n" + expiry; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); var signature = Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign))); var sasToken = string.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", HttpUtility.UrlEncode(resourceUri), HttpUtility.UrlEncode(signature), expiry, keyName); return sasToken; } } ``` Here we use `FirebaseInstanceId.Instance.Token` to get the token and then we send a PUT HTTP Request to register our device with `NotificationHub`. You can find more details here: [Notification Hubs REST API Methods](https://learn.microsoft.com/en-us/previous-versions/azure/reference/mt621153(v=azure.100)). Now your device is registered. ### Setting up the Receivers Now, the last step is to define our receiver. You need to set up receivers to handle notifications pushed to your app. For Android, use `FirebaseMessagingService`. Override `OnMessageReceived()` to define how the notifications should be handled: ``` [Service(Exported = false)] [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })] public class PushNotificationFirebaseMessagingService : FirebaseMessagingService { public override void OnMessageReceived(RemoteMessage p0) { base.OnMessageReceived(p0); var receivedNotification = p0.GetNotification(); // implement your logic here... } } ``` ## This app is not authorized to use Firebase Authentication Firebase auth was working fine, but the debug build suddenly started failing without any change of code, logging the following message > D/PhoneAuthActivity( 7392): signInWithCredential:failure:com.google.firebase.auth.FirebaseAuthException: This app is not authorized to use Firebase Authentication. Please verifythat the correct package name and SHA-1 are configured in the Firebase Console. \[ App validation failed \]. Using a `try ... catch` the error detail is the following: ``` LanguageInUse.Platforms.Android.Notification.DeviceInstallationService.RegisterDevice(String notificationHubNamespace, String notificationHub, String key) in C:\Projects\ERDevOps\LIUApp\LanguageInUse\Platforms\Android\Notification\DeviceInstallationService.cs:line 30 --- End of managed Java.IO.IOException stack trace --- java.io.IOException: java.util.concurrent.ExecutionException: java.io.IOException: FIS_AUTH_ERROR at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:626) at com.google.firebase.messaging.FirebaseMessaging.lambda$getToken$4$com-google-firebase-messaging-FirebaseMessaging(FirebaseMessaging.java:382) at com.google.firebase.messaging.FirebaseMessaging$$ExternalSyntheticLambda9.run(Unknown Source:4) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2) at java.lang.Thread.run(Thread.java:1012) Caused by: java.util.concurrent.ExecutionException: java.io.IOException: FIS_AUTH_ERROR at com.google.android.gms.tasks.Tasks.zza(com.google.android.gms:play-services-tasks@@18.0.2:5) at com.google.android.gms.tasks.Tasks.await(com.google.android.gms:play-services-tasks@@18.0.2:8) at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:624) ... 9 more Caused by: java.io.IOException: FIS_AUTH_ERROR at com.google.firebase.messaging.GmsRpc.handleResponse(GmsRpc.java:309) at com.google.firebase.messaging.GmsRpc.lambda$extractResponseWhenComplete$0$com-google-firebase-messaging-GmsRpc(GmsRpc.java:320) at com.google.firebase.messaging.GmsRpc$$ExternalSyntheticLambda0.then(Unknown Source:2) at com.google.android.gms.tasks.zzc.run(com.google.android.gms:play-services-tasks@@18.0.2:3) at androidx.profileinstaller.ProfileInstallReceiver$$ExternalSyntheticLambda0.execute(Unknown Source:0) at com.google.android.gms.tasks.zzd.zzd(com.google.android.gms:play-services-tasks@@18.0.2:1) at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.2:5) at com.google.android.gms.tasks.zzw.zzb(com.google.android.gms:play-services-tasks@@18.0.2:3) at com.google.android.gms.tasks.zzc.run(com.google.android.gms:play-services-tasks@@18.0.2:8) at com.google.android.gms.cloudmessaging.zzz.execute(Unknown Source:0) at com.google.android.gms.tasks.zzd.zzd(com.google.android.gms:play-services-tasks@@18.0.2:1) at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.2:5) at com.google.android.gms.tasks.zzw.zzb(com.google.android.gms:play-services-tasks@@18.0.2:3) at com.google.android.gms.tasks.TaskCompletionSource.setResult(com.google.android.gms:play-services-tasks@@18.0.2:1) at com.google.android.gms.cloudmessaging.zzp.zzd(com.google.android.gms:play-services-cloud-messaging@@17.0.0:3) at com.google.android.gms.cloudmessaging.zzr.zza(com.google.android.gms:play-services-cloud-messaging@@17.0.0:2) at com.google.android.gms.cloudmessaging.zzf.handleMessage(com.google.android.gms:play-services-cloud-messaging@@17.0.0:14) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loopOnce(Looper.java:205) at android.os.Looper.loop(Looper.java:294) at android.app.ActivityThread.main(ActivityThread.java:8177) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971) ``` If I added to the `PushNotificationFirebaseMessagingService.cs` the plugin for local notification called `Plugin.LocalNotification`, the error changed in the following one: ``` at LanguageInUse.Platforms.Android.Notification.DeviceInstallationService.RegisterDevice(String notificationHubNamespace, String notificationHub, String key) in C:\Projects\ERDevOps\LIUApp\LanguageInUse\Platforms\Android\Notification\DeviceInstallationService.cs:line 30 --- End of managed Java.IO.IOException stack trace --- java.io.IOException: java.util.concurrent.ExecutionException: java.io.IOException: SERVICE_NOT_AVAILABLE at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:626) at com.google.firebase.messaging.FirebaseMessaging.lambda$getToken$4$com-google-firebase-messaging-FirebaseMessaging(FirebaseMessaging.java:382) at com.google.firebase.messaging.FirebaseMessaging$$ExternalSyntheticLambda9.run(Unknown Source:4) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:487) at java.util.concurrent.FutureTask.run(FutureTask.java:264) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:307) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644) at com.google.android.gms.common.util.concurrent.zza.run(com.google.android.gms:play-services-basement@@18.2.0:2) at java.lang.Thread.run(Thread.java:1012) Caused by: java.util.concurrent.ExecutionException: java.io.IOException: SERVICE_NOT_AVAILABLE at com.google.android.gms.tasks.Tasks.zza(com.google.android.gms:play-services-tasks@@18.0.2:5) at com.google.android.gms.tasks.Tasks.await(com.google.android.gms:play-services-tasks@@18.0.2:8) at com.google.firebase.messaging.FirebaseMessaging.blockingGetToken(FirebaseMessaging.java:624) ... 9 more Caused by: java.io.IOException: SERVICE_NOT_AVAILABLE at com.google.android.gms.cloudmessaging.zzv.then(com.google.android.gms:play-services-cloud-messaging@@17.0.0:5) at com.google.android.gms.tasks.zzc.run(com.google.android.gms:play-services-tasks@@18.0.2:3) at com.google.android.gms.cloudmessaging.zzz.execute(Unknown Source:0) at com.google.android.gms.tasks.zzd.zzd(com.google.android.gms:play-services-tasks@@18.0.2:1) at com.google.android.gms.tasks.zzr.zzb(com.google.android.gms:play-services-tasks@@18.0.2:5) at com.google.android.gms.tasks.zzw.zza(com.google.android.gms:play-services-tasks@@18.0.2:4) at com.google.android.gms.tasks.TaskCompletionSource.setException(com.google.android.gms:play-services-tasks@@18.0.2:1) at com.google.android.gms.cloudmessaging.zzp.zzc(com.google.android.gms:play-services-cloud-messaging@@17.0.0:3) at com.google.android.gms.cloudmessaging.zzm.zzb(com.google.android.gms:play-services-cloud-messaging@@17.0.0:8) at com.google.android.gms.cloudmessaging.zzm.zza(com.google.android.gms:play-services-cloud-messaging@@17.0.0:1) at com.google.android.gms.cloudmessaging.zzm.zzd(com.google.android.gms:play-services-cloud-messaging@@17.0.0:1) at com.google.android.gms.cloudmessaging.zzi.run(Unknown Source:2) ... 7 more Caused by: com.google.android.gms.cloudmessaging.zzq: Timed out while binding at com.google.android.gms.cloudmessaging.zzm.zzb(com.google.android.gms:play-services-cloud-messaging@@17.0.0:6) ... 10 more ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-12.png?resize=640%2C239&ssl=1)![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-13.png?resize=640%2C306&ssl=1)## Topic sync or token retrieval failed on hard failure exceptions: FIS\_AUTH\_ERROR. Won’t retry the operation. I fought this error for about a week and I couldn’t understand what the problem was. In my case, the problem was related to the configuration in the Google Cloud under credentials. ![Google Console Api & Service credentials](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-24.png?resize=640%2C567&ssl=1) So, my advice is to try your app without any restriction and then add the restrictions and test your app. **Categories:** .NET8, MAUI **Tags:** android, azure-notification-hub, maui, net8 **Hashtags:** maui, net8 --- ### [MAUI Push Notifications using Azure Notification Hub for iOS](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-ios/) **Published:** March 3, 2024 **Author:** Enrico **Excerpt:** Here the new post where I explain how to implement the Push Notifications in MAUI using Azure Notification Hub for iOS without external plugin **Content:** After a few weeks of fighting with the Push Notifications in NET8 MAUI using Azure Notification Hub for iOS and it wasn’t working, I finally found the way to do it. In this post, I will explain every step and how to implement the push notification, jumping from the Apple developer website, Azure portal and the code in C#. So, I show you everything I discovered **without using external plugin** but only what MAUI offers and `HttpClient`. I split this topic in a few posts: - [MAUI Push Notifications using Azure Notification Hub for Windows](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/) - [MAUI Push Notifications using Azure Notification Hub for Android](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-android/) - [MAUI Push Notifications using Azure Notification Hub for iOS](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub-for-ios/) Now, it is time to start the journey and discover how to do it. ## Configure push notification in the Apple Developer portal First, I have to create the certificates from the Apple Developer portal specifically for the application. The prerequisites for that are: - An active [Apple Developer](https://developer.apple.com/) account. - A Mac running Xcode, along with a valid developer certificate, is installed into your Keychain. - An iPhone or iPad running iOS version 10 or later. - Your physical device registered in the [Apple Portal](https://developer.apple.com/) and associated with your certificate. ### Generate the certificate-signing request file The Apple Push Notification Service (APNS) uses certificates to authenticate your push notifications. Follow these instructions to create the necessary push certificate to send and receive notifications. For more information on these concepts, see the official [Apple Push Notification Service](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html) documentation. Generate the Certificate Signing Request (CSR) file, which Apple uses to generate a signed push certificate: 1. On your Mac, run the Keychain Access tool. It can be opened from the **Utilities** folder or the **Other** folder on the Launchpad. 2. The application is asking you what I want to open. You should see the following window. Select **Open Keychain access**. ![KeyChain request - MAUI Push Notifications using Azure Notification Hub for iOS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/mac-keychain-select.png?resize=640%2C781&ssl=1)Keychain request 3. Select **Keychain Access**, expand **Certificate Assistant**, and then select **Request a Certificate from a Certificate Authority**. ![Request a Certificate From a Certificate Authority from my Mac - MAUI Push Notifications using Azure Notification Hub for iOS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/mac-keychain-request-certificate.png?resize=640%2C253&ssl=1)Request a Certificate From a Certificate Authority from my Mac 4. Select your **User Email Address**, enter your **Common Name** value, make sure that you specify **Saved to disk**, and then select **Continue**. Leave **CA Email Address** blank as it isn’t required. ![KeyChain Certificate Assistant on my Mac](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/mac-keychain-certificate-assistant.png?resize=640%2C487&ssl=1)Keychain Certificate Assistant on my Mac 5. Enter a name for the CSR file in **Save As**, select the location in **Where**, and then select **Save**. This action saves the CSR file in the selected location. The default location is **Desktop**. Remember the location chosen for the file. ![KeyChain Certificate Assistant saves the request](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/mac-keychain-save.png?resize=640%2C487&ssl=1)Keychain Certificate Assistant saves the request Next, register your app with Apple, enable push notifications, and upload the exported CSR to create a push certificate. ### Register your app for push notifications To send push notifications to an iOS app, register your application with Apple, and also register for push notifications. 1. If you haven’t already registered your app, browse to the [iOS Provisioning Portal](https://go.microsoft.com/fwlink/p/?LinkId=272456) at the Apple Developer Center. Sign in to the portal with your Apple ID, and select **Identifiers**. Then select **+** to register a new app. ![Apple Developer website: Certificates, Identifiers & Profile](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image.png?resize=640%2C181&ssl=1)Apple Developer website: Certificates, Identifiers & Profile 2. On the **Register a New Identifier** screen, select the **App IDs** radio button. Then select **Continue**. ![Apple Developer website: Certificates, Identifiers & Profile](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-1.png?resize=640%2C227&ssl=1)Apple Developer website: Certificates, Identifiers & Profile 3. Update the following three values for your new app, and then select **Continue**: - **Description**: Type a descriptive name for your app. - **Bundle ID**: Enter a Bundle ID of the form **Organization Identifier.Product Name** as mentioned in the [App Distribution Guide](https://help.apple.com/xcode/mac/current/#/dev91fe7130a). The **Organization Identifier** and **Product Name** values must match the organization identifier and product name you use when you create your Xcode project. In the following screenshot, the **NotificationHubs** value is used as an organization identifier and the **GetStarted** value is used as the product name. Make sure the **Bundle Identifier** value matches the value in your Xcode project so that Xcode uses the correct publishing profile.![Register app ID](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image6.png?w=640&ssl=1) - **Push Notifications**: Check the **Push Notifications** option in the **Capabilities** section.![Register new app ID](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image7.png?w=640&ssl=1)This action generates your App ID and requests that you confirm the information. Select **Continue**, then select **Register** to confirm the new App ID.![Confirm new App ID](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image8.png?w=640&ssl=1)After you select **Register**, you see the new App ID as a line item on the **Certificates, Identifiers & Profiles** page. 4. In the **Certificates, Identifiers & Profiles** page, under **Identifiers**, locate the App ID line item that you just created, and select its row to display the **Edit your App ID Configuration** screen. ### Create a certificate for Notification Hubs > With the release of iOS 13, you can only receive silent notifications using token based authentication. If you are using certificate-based authentication for your APNS credentials, you must switch to using [token-based authentication](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/establishing_a_token-based_connection_to_apns). A certificate is required to enable the notification hub to work with **APNS**. This can be done in one of two ways: - Create a **.p12** file that can be uploaded directly to Notification Hubs. - Create a **.p8** file that can be used for [token-based authentication](https://learn.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-http2-token-authentication) (the newer approach). The second option has a number of benefits compared to using certificates, as documented in [Token-based (HTTP/2) authentication for APNS](https://learn.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-http2-token-authentication). However, steps are provided for both approaches. #### Option 1: Create a .p12 push certificate that can be uploaded directly to Notification Hubs 1. Scroll down to the checked **Push Notifications** option, and then select **Configure** to create the certificate. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-2.png?resize=640%2C305&ssl=1) 2. The **Apple Push Notification service SSL Certificates** window appears. Select the **Create Certificate** button in the **Development SSL Certificate** section. The **Create a new Certificate** screen is displayed. Follow the same process for the **Production** certificate. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-3.png?resize=640%2C470&ssl=1) 3. Select **Choose File**, browse to the location at which you saved the CSR file from the first task, and then double-click the certificate name to load it. Then select **Continue**. 4. After the portal creates the certificate, select the **Download** button. Save the certificate, and remember the location to which it’s saved. The certificate is downloaded and saved in your **Downloads** folder. By default, the downloaded development certificate is named **aps\_development.cer**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-4.png?resize=640%2C270&ssl=1) 5. Double-click the downloaded push certificate **aps\_development.cer**. This action installs the new certificate in the Keychain, as shown in the following image ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/keychain-certificates.png?resize=640%2C418&ssl=1) 5. In Keychain Access, right-click the new push certificate that you created in the **Certificates** category. Select **Export**, name the file, select the **.p12** format, and then select **Save**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/keychain-export.png?resize=640%2C295&ssl=1) You can choose to protect the certificate with a password, but this is optional. Click **OK** if you want to bypass password creation. Make a note of the file name and location of the exported .p12 certificate. They are used to enable authentication with APNS. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/keychain-save-2.png?resize=640%2C418&ssl=1) ### Option 2: Create a .p8 certificate that can be used for token-based authentication 1. Make note of the following details: - **App ID Prefix** (this is a **Team ID**) - **Bundle ID** 2. Back in **Certificates, Identifiers & Profiles**, click **Keys**. If you already have a key configured for **APNS**, you can re-use the .p8 certificate that you downloaded right after it was created. If so, you can ignore steps 3 through 5. 3. Click the **+** button (or the **Create a key** button) to create a new key. 4. Provide a suitable **Key Name** value, check the **Apple Push Notifications service (APNS)** option, and then click **Continue**, followed by **Register** on the next screen. 5. Click **Download** and then move the **.p8** file (prefixed with `AuthKey_`) to a secure local directory, then click **Done**. **Important**: Be sure to keep your .p8 file in a secure place (and save a backup). After downloading your key, it cannot be re-downloaded; the server copy is removed. 6. On **Keys**, click on the key that you just created (or an existing key if you have chosen to use that instead). 7. Make note of the **Key ID** value. 8. Open your .p8 certificate in a suitable application of your choice, such as [Visual Studio Code](https://code.visualstudio.com/), then make note of the key value. This is the value between **—–BEGIN PRIVATE KEY—–** and **—–END PRIVATE KEY—–** ``` -----BEGIN PRIVATE KEY----- -----END PRIVATE KEY----- ``` At the end of these steps you should have the following information for use later in [Configure your notification hub with APNS information](https://learn.microsoft.com/en-us/azure/notification-hubs/ios-sdk-get-started#configure-the-notification-hub-with-apns-information): - **Team ID** (see step 1) - **Bundle ID** (see step 1) - **Key ID** (see step 7) - **Token value** (the .p8 key value, see step 8) ## Configure the notification hub with APNS information Under **Notification Services**, select **Apple (APNS)**, then follow the appropriate steps based on the approach you chose previously in the Creating a Certificate for Notification Hubs section. ### Option 1: Use a .p12 push certificate - Select **Certificate**. - Select the file icon. - Select the .p12 file that you exported earlier, and then select **Open**. - If required, specify the correct password. - Select **Sandbox** mode. - **Save** ### Option 2: Use token-based authentication 1. Select **Token**. 2. Enter the following values that you acquired earlier: - **Key ID** - **Bundle ID** - **Team ID** - **Token** 3. Choose **Sandbox** 4. Select **Save**. You’ve now configured your notification hub with APNS. You also have the connection strings needed to register your app and send push notifications. ### Create a provisioning profile 1. Return to the [iOS Provisioning Portal](https://go.microsoft.com/fwlink/p/?LinkId=272456), select **Certificates, Identifiers & Profiles**, select **Profiles** from the left menu, and then select **+** to create a new profile. The **Register a New Provisioning Profile** screen appears. 2. Select **iOS App Development** under **Development** as the provisioning profile type, and then select **Continue**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-5.png?resize=640%2C254&ssl=1) 3. Next, select the app ID you created from the **App ID** drop-down list, then select **Continue**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-6.png?resize=640%2C254&ssl=1) 4. In the **Select certificates** window, select the development certificate that you use for code signing, and select **Continue**. This certificate isn’t the push certificate you created. If one does not exist, you must create it. If a certificate does exist, skip to the next step. To create a development certificate if one does not exist: 1. If you see **No Certificates are available**, select **Create Certificate**. 2. In the **Software** section, select **Apple Development**. Then select **Continue**. 3. In the **Create a New Certificate** screen, select **Choose File**. 4. Browse to the **Certificate Signing Request** certificate you created earlier, select it, and then select **Open**. 5. Select **Continue**. 6. Download the development certificate, and remember the location in which it’s saved. 5. Return to the **Certificates, Identifiers & Profiles** page, select **Profiles** from the left menu, and then select **+** to create a new profile. The **Register a New Provisioning Profile** screen appears. 6. In the **Select certificates** window, select the development certificate that you just created. Then select **Continue**. 7. Next, select the devices to use for testing, and select **Continue**. 8. Finally, choose a name for the profile in **Provisioning Profile Name**, then select **Generate**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-7.png?resize=640%2C305&ssl=1) 9. When the new provisioning profile is created, select **Download**. Remember the location in which it’s saved. 10. Browse to the location of the provisioning profile, and then double-click it to install it on your Xcode development machine. ## Configure the Azure Notification Hub In the first [post of this series](https://puresourcecode.com/dotnet/maui/maui-push-notifications-using-azure-notification-hub/) about the implementation of Push Notifications in MAUI using Azure Notification Hub, I have created an Azure Notification Hub. From the **Apple Developer** website, I can download the **Apple Push Notification service SSL Certificates** for **development** and **production**. ![Apple Push Notification service SSL Certificates - MAUI Push Notifications using Azure Notification Hub for iOS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/Screenshot-2024-02-23-at-00.58.25.png?resize=640%2C779&ssl=1)Apple Push Notification service SSL Certificates Now, go to the **Azure portal** and open the **Notification Hub**. Here I can upload the certificate that I created from the Apple Developer portal. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/Screenshot-2024-03-03-at-11.08.31.png?resize=640%2C265&ssl=1)One important thing I learned and blocked me for more than 2 weeks is that I can import only one certificate. You see in the **Application Mode**, there are 2 options: - Production - Sandbox What I did was to upload first the certificate for Production and Save and then the one for Sandbox and save. I was thinking that Azure saves both certificates but it was only my idea and I was wrong. Only one certificate is available. Now, I use the **Sandbox** and this is important because I have to configure the `Entitlements.plist` accordingly. ## Add Entitlements.plist to the iOS Platform To add a new entitlements file to your .NET MAUI app project, add a new XML file named *Entitlements.plist* to the *Platforms\\iOS* folder of your app project. Then add the following XML to the file: ``` ``` If I want to add the entitlement for push notifications, I have to add those lines ``` keychain-access-groups $(AppIdentifierPrefix)yourappnamespace aps-environment development ``` The `keychain-access-groups` is the value that identifies your application. The `aps-environment` defines the environment for the push notification. It can have 2 values: **development** or **production**. This is explained in the [Apple documentation](https://developer.apple.com/documentation/bundleresources/entitlements/aps-environment?language=objc). In the case of this post, I’m using development. When I deploy the application on the Apple Store, I should change it. That has made me think that I have to create 2 Notification Hubs: one for development and one for production. Only Apple has 2 different settings. Windows and Android don’t distinguish between environments. If you want to read more about the Entitlements.plist, there is a [Microsoft documentation](https://learn.microsoft.com/en-us/dotnet/maui/ios/entitlements?view=net-maui-8.0&tabs=vs) that explains every key. Another thing it is important to know and I haven’t found anything about it is what **Properties** I have to set for the `Entitlements.plist`. There are no particular properties to set but I have to change the project properties as I describe in the next section. ![Entitlements.plist properties](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-9.png?resize=299%2C232&ssl=1)Entitlements.plist properties ### Consume entitlements A .NET MAUI iOS app must be configured to consume the entitlements defined in the *Entitlements.plist* file. So, in Visual Studio follow those steps: 1. In **Solution Explorer**, right-click on your .NET MAUI app project and select **Properties**. Then, navigate to the **iOS > Bundle Signing** tab. 2. In the **Bundle Signing** settings, click the **Browse…** button for the **Custom Entitlements** field. 3. In the **Custom Entitlements** dialog, navigate to the folder containing your *Entitlements.plist* file, select the file, and click the **Open** button. 4. In the project properties, the **Custom Entitlements** field will be populated with your entitlements file: ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/03/image-8.png?resize=640%2C508&ssl=1)I think there is a bug in Visual Studio. Sometimes, when I set the **Custom Entitlements** and deploy on a device, I get some weird errors like ``` error MT1045: Failed to execute 'devicectl': 'devicectl -j /var/folders/dm/bwmxpbzn6bvdsyy73c_b453w0000gn/T/tmpBkAavG.tmp device install app --device "Enrico???s iPhone" /Users/enrico/Library/Caches/Xamarin/mtbs/builds/LanguageInUse/1fa03704bb15e35c6f47a701d9d92131e3e0740198296a93338bb3c829bc9cf7/bin/Debug/net8.0-ios/ios-arm64/device-builds/iphone15.2-17.3.1/LanguageInUse.app' returned the exit code 1. 0 ``` After a few days, I saw that not only the **Custom Entitlements** but also the **Custom Resource Rules** had the same configuration but I haven’t set it. I deleted the Custom Resources Rules and I could deploy the application. ## The code Finally, I can go into my code to implement the Push Notification for iOS. First step is to ask the permissions to the user to send push notification. ### Require user authorization Now, open your `AppDelegate.cs` and in the `FinishedLaunching` add the request for the authorization. This will open a request from the system to the user if it wants to authorize your application to receive the notifications. ``` public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { var result = base.FinishedLaunching(application, launchOptions); var authOptions = UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound; UNUserNotificationCenter.Current.RequestAuthorization(authOptions, (granted, error) => { if (granted && error == null) { this.InvokeOnMainThread(() => { UIApplication.SharedApplication.RegisterForRemoteNotifications(); UNUserNotificationCenter.Current.Delegate = this; }); } }); return result; } ``` If the user authorizes your app to receive the push notification, the function **RegisterForRemoteNotifications** is called. This is an important function that is coming from Apple. ### Configure RegisterForRemoteNotifications I can’t stress how much pain was to understand all the processes until here and how much time I spent in debugging without finding a reason why my code wasn’t working. I must use this function to set the push notification. First, I have to add and define this function in the code in this way ``` [Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")] public async void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken) { // ... } ``` In the `deviceToken` iOS gives to me what I have to use to set the push channel in the call to the Azure Notification Hub. This `NSData` is an Apple structure that contains an array of bytes. The problem was how to transform this sequence of bytes in a correct value to use. Fost forward to the solution, this is how to convert the `deviceToken` to the string I must use to register the device with the Azure Notification Hub. ``` string token = null!; if (deviceToken.Length > 0) { if (UIDevice.CurrentDevice.CheckSystemVersion(13, 0)) { var data = deviceToken.ToArray(); token = BitConverter .ToString(data) .Replace("-", "") .Replace("\"", ""); } else if (!string.IsNullOrEmpty(deviceToken.Description)) { token = deviceToken.Description.Trim(''); } } ``` It is important to check what version of the iOS operating system the user is currently on. If the version is a new one, I have to use `BitConverter` to get the string I need. Now, I have to find the device Id. This is quite straightforward after spending a few hours googling and binging. This is the code ``` string deviceId = UIDevice.CurrentDevice.IdentifierForVendor.AsString(); ``` Finally, I can call the Azure Notification Hub to register the device. ``` var deviceInstallation = new { InstallationId = deviceId, Platform = "apns", PushChannel = token }; using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("x-ms-version", "2015-01"); httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", CreateToken($"https://{Constants.PushNotificationHubNamespace}.servicebus.windows.net", "DefaultListenSharedAccessSignature", Constants.PushNotificationSecret)); var t = await httpClient.PutAsJsonAsync($"https://{Constants.PushNotificationHubNamespace}.servicebus.windows.net/" + $"{Constants.PushNotificationHub}/installations/{deviceInstallation.InstallationId}?api-version=2015-01", deviceInstallation); ``` ### Receive the notification Now, I have to tell iOS what has to happen when a message from push notification arrives. For that, there is a specific function called **WillPresentNotification** and my basic implementation is the following ``` [Export("userNotificationCenter:willPresentNotification:withCompletionHandler:")] public void WillPresentNotification(UNUserNotificationCenter center, UNNotification notification, Action completionHandler) { var userInfo = notification.Request.Content.UserInfo; // tell the system to display the notification in a standard way // or use None to say app handled the notification locally completionHandler(UNNotificationPresentationOptions.Alert); } ``` ## Wrap up In conclusion, after a few long weeks, I finally managed to have my project working and receiving push notifications from Azure Notification Hub. I created those posts because I couldn’t find much documentation and the process, in particular for iOS, was very painful. Hopefully, this can help someone else avoid the mistakes I made. Please let me know in the comment below or the [Forum](https://puresourcecode.com/forum/) if you have any questions, comments or suggestions. If you want to support my job or just send a token of appreciation, you can consider paying me a beer using the [Sponsor on GitHub](https://github.com/sponsors/erossini). **Categories:** .NET8, MAUI **Tags:** azure-notification-hub, ios, maui, net8 **Hashtags:** maui, net8 --- ### [Set safe areas for iOS in MAUI](https://puresourcecode.com/dotnet/maui/set-safe-areas-for-ios-in-maui/) **Published:** February 28, 2024 **Author:** Enrico **Excerpt:** When you create an application for iOS in MAUI, we have to set the safe areas if we want to use the full screen for your application. **Content:** When you create an application for [iOS](https://puresourcecode.com/tag/ios/) in [MAUI](https://puresourcecode.com/tag/maui/), we have to set the safe areas if we want to use the full screen for your application. The safe areas in iOS are one at the top and one at the bottom. ![Where are the safe areas? - Set safe area for iOS in MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-32.png?resize=437%2C1012&ssl=1) At the top of the screen, you have the clock and other common system information plus the notch in the newest models. At the bottom of the screen, you have the home bar. ## Issues By default, .NET MAUI will take the safe area into account. So the use of the platform-specific UseSafeArea is to disable safe areas. Currently, setting UseSafeArea to false doesn’t change the behaviour (although it should), which is a bug. Also, see the issue on the MAUI [Github](https://github.com/dotnet/maui/issues/5856). There’s also an `IgnoreSafeArea` property you can set to achieve the same thing. However, it’s no longer working in .NET 7, see the following issue: To fix the problem you need to add `IgnoreSafeArea="True"` to your Grid or StackLayout and `ios:Page.UseSafeArea="False"` to your page. Documentation about disabling safe areas on iOS can be found here: Unfortunately, this is not enough to use the safe areas. ## Solution After spending a few hours, I think the best solution is to write a platform-specific code. First, I have to create a new class in the root of the project that I’m going to call `DeviceSafeInsetsService` ``` namespace LanguageInUse { public partial class DeviceSafeInsetsService { public partial double GetSafeAreaTop(); public partial double GetSafeAreaBottom(); } } ``` In the code above, I included the `namespace` for a reason. When I will implement the platform-specific code, the implementation must be in the same namespace. If not, it won’t work. Another important thing is that when you create without implementation, you get some errors like > Error CS8795 Partial method ‘DeviceSafeInsetsService.GetSafeAreaTop()’ must have an implementation part because it has accessibility modifiers. LanguageInUse (net8.0-windows10.0.19041.0) C:\\Projects\\ERDevOps\\LIUApp\\LanguageInUse\\DeviceSafeInsetsService.cs 11 Active This is normal. You have to implement this class in every platform. So, in the Windows and Android platforms (if you have your project also for Tizen and macCatalyst do it the same there), add a new class `DeviceSafeInsetsService` and just return a random value ``` namespace LanguageInUse { public partial class DeviceSafeInsetsService { public partial double GetSafeAreaBottom() { return 0; } public partial double GetSafeAreaTop() { return 0; } } } ``` ## iOS implementation Now, generate a partial file on Platform iOS and implement it. This file is placed in the **Project/Platform/iOS** folder and what I want to mention is this file is a partial file, so the namespace should be the same as the file above. ``` namespace LanguageInUse { public partial class DeviceSafeInsetsService { public partial double GetSafeAreaBottom() { if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) { UIWindow window = UIApplication.SharedApplication.Delegate.GetWindow(); var bottomPadding = window.SafeAreaInsets.Bottom; return bottomPadding; } return 0; } public partial double GetSafeAreaTop() { if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) { UIWindow window = UIApplication.SharedApplication.Delegate.GetWindow(); var TopPadding = window.SafeAreaInsets.Top; return TopPadding; } return 0; } } } ``` ## How to use it Now, using the code above will force you to update your pages and probably add more namespace to avoid ambiguities. First, in the code behind for each page, add at the top this code to include the specific package for iOS. ``` #if IOS using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; #endif ``` If for example in your code you have some reference to particular components like for example `Application` or `ListView`, you have to specify the right namespace. So, for example: - **Application** becomes **Microsoft.Maui.Controls.Application** - **ListView** becomes **Microsoft.Maui.Controls.ListView** After that, you have to add the Padding in the `OnAppearing` function like here ``` protected override async void OnAppearing() { base.OnAppearing(); DeviceSafeInsetsService d = new DeviceSafeInsetsService(); double topArea = d.GetSafeAreaTop(); double bottomArea = d.GetSafeAreaBottom(); #if IOS var safeInsets = On().SafeAreaInsets(); safeInsets.Bottom = -bottomArea; safeInsets.Top = -topArea; Padding = safeInsets; #endif } } ``` Run and your app is using the full screen. ## Wrap up In conclusion, in this post, I show you how to set the safe areas for iOS in MAUI and [NET8](https://puresourcecode.com/tag/net8/). I hope you find this code useful. If you have any questions or comments, please use the [forum](https://puresourcecode.com/forum/). **Categories:** .NET8, MAUI **Tags:** ios, maui, net8, notch, safe-areas **Hashtags:** ios, maui, net8 --- ### [Deploy MAUI apps on a real device](https://puresourcecode.com/dotnet/visual-studio/deploy-maui-apps-on-a-real-device/) **Published:** February 27, 2024 **Author:** Enrico **Excerpt:** Developing applications with NET8 MAUI, we want to deploy apps on a real device. Here are all the necessary steps without wasting time. **Content:** Developing applications with NET8 MAUI, we want to deploy and test apps on real devices. The configuration for Apple is not easy and requires a lot of steps. Microsoft documentation is not updated with the latest version of Visual Studio and the Apple Developer portal. So, here are my notes on how to do it without wasting time. ## Windows and Windows First, the easy part. Run and test applications from Visual Studio to Windows – assuming you are using Visual Studio on Windows (Visual Studio for Mac is not available anymore) – you have just to press run. Sometimes, Visual Studio can require you to deploy the application to your machine before running it. ### Android For Android, the deployment is quite easy too. You can connect your Android device to the machine with Visual Studio and then deploy the application to the device when Visual Studio recognizes it. Another solution is to create an instance of a simulator. ![Example of a list of Android Emulators](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-28.png?resize=640%2C291&ssl=1)Example of a list of Android Emulators If you can’t see the device in the list, you can try to restart `adb` or check the minimum API version of the application. If the application is set to use a higher API of your device, the device won’t be listed. ## Connect your account on Xcode First, I have to add my account in **Xcode** on my **iMac**. For that, open Xcode and then click on **Settings** under the main menu. I see this window (in this case you see my account). Add your account by clicking the **+** at the bottom left. Remember to buy an account as a developer from Apple. ![Account window in Xcode - Deploy MAUI apps on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/Xcode-Accounts.png?resize=640%2C451&ssl=1)From here, click on **Download Manual Profiles**. Then, click on **Manage Certificates** and create a certificate for **development** and another one for **distribution**. ## Login with Apple account in Visual Studio Now, the first thing to do is connect your Visual Studio with Apple. The steps for Visual Studio are explained in the [Microsoft documentation](https://learn.microsoft.com/en-us/xamarin/cross-platform/macios/apple-account-management?context=xamarin%2Fios&tabs=windows) but it is not updated with the last version of Visual Studio or the new Apple Developer website. So, here are some hints. > You will need to be [Paired to a Mac build host](https://learn.microsoft.com/en-us/xamarin/ios/get-started/installation/windows/connecting-to-mac/) before proceeding Under the menu **Tools > Options**, search for **Apple Accounts**. Then, click on **Add**. Now, you should see a new window like the following screenshot. ![Individual Account configuration in Visual Studio - Deploy MAUI app on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-25.png?resize=640%2C371&ssl=1)Individual Account configuration in Visual Studio The info to insert are coming from the Apple Developer website and in order to generate keys, you must have an **Admin** account in **App Store Connect**. You may generate multiple API keys with any roles you choose. Log in to App Store Connect to generate an API key to use with the App Store Connect API. 1. Select Users and Access, and then select the API Keys tab. 2. Click **Generate API Key** or the **Add** (**+**) button. 3. Enter a name for the key. The name is for your reference only and is not part of the key itself. 4. Under Access, select the role for the key. 5. Click **Generate**. ![Users and Access - Integration to create the API key for Visual Studio - Deploy MAUI app on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-26.png?resize=640%2C155&ssl=1)Users and Access – Integration to create the API key for Visual Studio The new key’s name, key ID, a download link, and other information appear on the page. Fill out the form in Visual Studio using the info from this page: - **Name** is your name (this is my guess) - **Issuer ID** is displayed after the API key is generated and you can copy it from the page - **Key ID** copied from the table in the page - **Private key path** select the path where the file is Here is an example of the table with the API key. ![Example of a API key created (in this case only for display) - Deploy MAUI apps on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-27.png?resize=640%2C63&ssl=1)Example of an API key created (in this case only for display) ## Create an application in Xcode Now, the next part is to create a dummy application in **Xcode** on your Mac to have all the required provisionings and certificates on your machine. For that, launch Xcode and create a new **App**. ![Xcode: select a new project - Deploy MAUI app on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/xcode-select-project.png?resize=640%2C428&ssl=1)Xcode: select a new project Then, fill out the form adding the **Project Name** you want to use but the most important thing to type carefully is the **Organization Identifier**. Project Name and Organization Identifier together are the **ApplicationId** (**Bundle ID** for Apple or **Package name** for Android). ![Xcode: select the product name - Deploy MAUI app on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/xcode-define-product-name.png?resize=640%2C428&ssl=1)Xcode: select the product name So, you are ready to select the **Team** and get your certificate. Only with those configurations, you will be able to deploy your applications on real devices. ![Xcode: signing & Capabilities - Deploy MAUI apps on a real device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/xcode-signing.png?resize=640%2C428&ssl=1)Xcode: signing & Capabilities ## Deploy to a real device from Visual Studio The first important action is to connect Visual Studio to the Mac machine (if you haven’t done so yet). If the connection is established, you see a chain on the Mac machine. ![Pair to Mac](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-29.png?resize=590%2C545&ssl=1) Fast forward to the deployment, you have to check the configuration of the project for Apple. Open the **Properties** of your project. ![iOS Bundle Signing for a project in Visual Studio](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-30.png?resize=640%2C436&ssl=1)iOS Bundle Signing for a project in Visual Studio Here you have 3 important settings: - Scheme: Manual Provisioning - Signing identity: Developer (Automatic) - Provisioning profile: Automatic Those settings are working for deployment to a simulator or a real device. ## Visual Studio raises an error MT1045 Failed to execute when deploying a MAUI app to a real device Visual Studio is connected to my iMac and the iPhone to the iMac. The deployment on the Simulator is working fine. When I deploy it on the iPhone, Visual Studio shows this error > error MT1045: Failed to execute ‘devicectl’: ‘devicectl -j /var/folders/dm/bwmxpbzn6bvdsyy73c\_b453w0000gn/T/tmpBkAavG.tmp device install app –device “Enrico???s iPhone” /Users/enrico/Library/Caches/Xamarin/mtbs/builds/LanguageInUse/1fa03704bb15e35c6f47a701d9d92131e3e0740198296a93338bb3c829bc9cf7/bin/Debug/net8.0-ios/ios-arm64/device-builds/iphone15.2-17.3.1/LanguageInUse.app’ returned the exit code 1. 0 In the *Output* window, I also notice this error: > NSLocalizedFailureReason = This app cannot be installed because its integrity could not be verified. NSLocalizedRecoverySuggestion = Failed to install embedded profile for com.languageinuse.app : 0xe800801f (Attempted to install a Beta profile without the proper entitlement.) I like to highlight that I deployed the application on the iPhone roughly a month ago without issues. I got the *Provisioning Profile* from the Apple Store and so on. The configuration on Visual Studio for iOS is the following. ![Project properties in Visual Studio](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-31.png?resize=640%2C393&ssl=1)Project properties in Visual Studio ### Solution First, that may be because you do not add the UDID of your iPhone device in the provisioning file. You may refer to [Add a device](https://learn.microsoft.com/en-us/dotnet/maui/ios/device-provisioning/manual-provisioning?view=net-maui-8.0&tabs=vs#add-a-device) and update your provisioning file to contain your new iPhone device. The other option is that for some reason the provisioning profile has changed. So, you have to download the new one in order to deploy it on a real device. **Categories:** MAUI, Visual Studio, Visual Studio **Tags:** apple, ios, maui, net8 **Hashtags:** apple, ios, maui, net8 --- ### [Remove bundle id in iOS development](https://puresourcecode.com/tools/ios/remove-bundle-id-in-ios-development/) **Published:** February 23, 2024 **Author:** Enrico **Excerpt:** Remove bundle id in iOS development when it tights to a different account of your. Spiler alert: clean the Library. **Content:** Some time ago I registered a new Apple Account and created an iOS app with it. I did not pay for that, i.e. the app could only ever be used for 7 days. I’ve never submitted that app to the app store. Nonetheless, the bundle identifier of the app was associated with that particular account. Sometime later I registered another Apple Account. This time with a paid subscription. When I attempted to sign the app, I created with the first account, Xcode complained as follows: > Failed to register bundle identifier. The app identifier “xxx” cannot be registered to your development team because it is not available. and > No profiles for ‘xxx’ were found. Xcode couldn’t file any iOS App Development provisioning profiles matching ‘xxx’. The bundle identifier is strictly associated with the provisioning profile of the first account. The way to solve this is to delete that particular provisioning profile. You can do so by going to the following folder and deleting the corresponding file in that folder: ``` ~/Library/MobileDevice/Provisioning Profiles/ ``` **Categories:** .NET8, iOS, MAUI **Tags:** development, ios, maui **Hashtags:** ios --- ### [Using SecureStorage with MAUI](https://puresourcecode.com/dotnet/maui/using-securestorage-with-maui/) **Published:** February 23, 2024 **Author:** Enrico **Excerpt:** Using SecureStorage with MAUI with the correct setup in your NET8 MAUI applications. Here the solution for iOS and Android. **Content:** I’m creating a new project called [LanguageInUse](https://languageinuse.com) with [NET8](https://puresourcecode.com/?s=net8) and [MAUI](https://puresourcecode.com/?s=maui) and I like to save the user credentials using the `SecureStorage`. Like in other cases, there are a few things that you can find but you must be done in order to have a working project. Let me show you how to configure your MAUI project properly. No settings are required for Windows. ## Apple So, looking at the documentation, I couldn’t find any explanation for why my application was crashing when it tried to save or delete something from the `SecureStorage`. The reason is that for iOS, I have to declare that I want to use secure storage. ## Add an Entitlements.plist file For this reason, I have to instruct the iOS to allow the application to use the `SecureStorage`. I have to add a new file. To add a new entitlements file to your .NET MAUI app project, add a new XML file named *Entitlements.plist* to the *Platforms\\iOS* folder of your app project. Then add the following XML to the file: ``` ``` Entitlements can be configured in Visual Studio by double-clicking the *Entitlements.plist* file to open it in the entitlements editor. 1. In **Solution Explorer**, double-click the *Entitlements.plist* file from the *Platforms > iOS* folder of your .NET MAUI app project to open it in the entitlements editor. 2. In the entitlements editor, select and configure any entitlements required by your app ![Entitlements in Visual Studio - Using SecureStorage with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-19.png?resize=640%2C733&ssl=1)Entitlements in Visual Studio Save the changes to your *Entitlements.plist* file to add the entitlement key/value pairs to the file. ### Consume entitlements A .NET MAUI iOS app must be configured to consume the entitlements defined in the *Entitlements.plist* file. 1. In **Solution Explorer**, right-click on your .NET MAUI app project and select **Properties**. Then, navigate to the **iOS > Bundle Signing** tab. 2. In the **Bundle Signing** settings, click the **Browse…** button for the **Custom Entitlements** field. 3. In the **Custom Entitlements** dialog, navigate to the folder containing your *Entitlements.plist* file, select the file, and click the **Open** button. 4. In the project properties, the **Custom Entitlements** field will be populated with your entitlements file: ![iOS Bundle Signing in Visual Studio - Using SecureStorage with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-8.png?resize=640%2C311&ssl=1)iOS Bundle Signing in Visual Studio ## Android The steps I have taken to resolve this error are: 1. Creating a `proguard.cfg` file in “Project/Platforms/Android/” in your file system. 2. In your .csproj file, set it as a `ProguardConfiguration`: ``` ``` ![Properties of the - Using SecureStorage with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/image-9.png?resize=302%2C382&ssl=1) 3. In your `proguard.cfg` file, add the following lines (you may be able to use less broad rules like in your original post but I have not tested this): ``` -keep class androidx.security.crypto.** { *; } -keep class com.google.crypto.tink.** { *; } ``` 4. Configure selective backup for MAUI essentials: [https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/storage/secure-storage?view=net-maui-8.0&tabs=android#selective-backup](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/storage/secure-storage?view=net-maui-8.0&tabs=android#selective-backup) - You will also want to setup data extraction rules for use on Android 12+. App center has a good example of how those should look: **Categories:** .NET8, MAUI **Tags:** maui, net8, secure-storage **Hashtags:** maui, net8 --- ### [Orange selected ListView item highlighted in MAUI](https://puresourcecode.com/dotnet/maui/orange-selected-listview-item-highlighted-in-maui/) **Published:** January 26, 2024 **Author:** Enrico **Excerpt:** In a MAUI project, when a selected ListView item is highlighted or selected, the background of this item is orange. Here the solution. **Content:** Working on my new MAUI project called [Language In Use](https://languageinuse.com), I face a funny issue with the orange item in a ListView when an item is selected or highlighted. I was asking this question in the past and the answer I got was that this was a bug, and it will be fixed with the [NET8](https://puresourcecode.com/category/dotnet/net8/). With the new version of the framework, the issue is still there. When I run my `NET8` `MAUI` app on an Android device, `ListView` displays the list of items correctly. When I tap on an item, the background is an orange colour, as you see in the following screenshot. [![An example of the issue with a ListView - Orange selected ListView item highlighted in MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/woliT.gif?w=640&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/woliT.gif?ssl=1)An example of the issue with a ListView Is a way to change the color of the selected item in a `ListView`? ## A suggestion (that didn’t work for me) I saw a few people refer to this solution but it didn’t work for me. I reported it just in case it could be one possible answer. Please let me know if it works for you. Under the `Android` folder, I added a new XML file called *style.xml* and set the property to `AndroidResource`. The content of this file is ``` ``` [![enter image description here](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/7Vatz.png?w=640&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/7Vatz.png?ssl=1)Then, I change the `MainActivity.cs` in particular, the theme as instructed. ``` [Activity(Theme = "@style/Test.MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] ``` ## The solution So, after looking around a lot and opening posts on Stackoverflow and GitHub, I found the solution that is coming from [Kenji Nagano](https://github.com/cat0363) in this post on [GitHub](https://github.com/dotnet/maui/issues/13812). First, this to check is if `ListView` you set the `CachingStrategy` and in particular `CachingStrategy="RecycleElement"`. If so, remove it. Then, the following solution will work. The first step, add `CustomViewCell.cs` to your project folder. ``` public class CustomViewCell : Microsoft.Maui.Controls.ViewCell { public static readonly BindableProperty SelectedBackgroundColorProperty = BindableProperty.Create( nameof(SelectedBackgroundColor), typeof(Color), typeof(CustomViewCell), Colors.White); public Color SelectedBackgroundColor { get { return (Color)GetValue(SelectedBackgroundColorProperty); } set { SetValue(SelectedBackgroundColorProperty, value); } } public CustomViewCell() { } } ``` ### Android implementation Then, in `Platforms/Android` folder, create the platform-specific implementation file `CustomViewCellHandler.cs`: ``` using Microsoft.Maui.Controls.Compatibility.Platform.Android; using Android.Graphics.Drawables; using AContext = Android.Content.Context; using AView = Android.Views.View; using AViewGroup = Android.Views.ViewGroup; using Microsoft.Maui.Controls.Platform; namespace MauiAppListViewTest.Platforms.Android { public class CustomViewCellHandler : Microsoft.Maui.Controls.Handlers.Compatibility.ViewCellRenderer { private AView pCellCore; private bool pSelected; private Drawable pUnselectedBackground; protected override AView GetCellCore(Cell item, AView convertView, AViewGroup parent, AContext context) { pCellCore = base.GetCellCore(item, convertView, parent, context); this.pSelected = false; this.pUnselectedBackground = pCellCore.Background; return pCellCore; } protected override void OnCellPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { base.OnCellPropertyChanged(sender, e); if (e.PropertyName == "IsSelected") { pSelected = !(pSelected); if (pSelected) { pCellCore.SetBackgroundColor( ((CustomViewCell)sender).SelectedBackgroundColor.ToAndroid()); } else { pCellCore.SetBackground(this.pUnselectedBackground); } } } } } ``` Now, register your handler like below in the `MauiProgram.cs`: ``` #if ANDROID using MauiAppListViewTest.Platforms.Android; #endif namespace MauiAppListViewTest { public static class MauiProgram { public static MauiApp CreateMauiApp() { .ConfigureMauiHandlers(handlers => { #if ANDROID handlers.AddHandler(); #endif }); ``` Don’t forget to setting `SelectedBackgroundColor="Green"` in **XAML**: ``` ``` ### iOS implementation Then, in `Platforms/`iOS folder, create the platform-specific implementation file `CustomViewCellHandler.cs`: ``` public class CustomViewCellHandler : ViewCellRenderer { public override UITableViewCell GetCell( Cell item, UITableViewCell reusableCell, UITableView tv) { var cell = base.GetCell(item, reusableCell, tv); cell.SelectedBackgroundView = new UIView { BackgroundColor = ((CustomViewCell)item).SelectedBackgroundColor.ToPlatform() }; return cell; } } ``` Again, I have to set the handle in the `MauiProgram.cs` like ``` #if IOS using MauiAppListViewTest.Platforms.iOS; #endif namespace MauiAppListViewTest { public static class MauiProgram { public static MauiApp CreateMauiApp() { .ConfigureMauiHandlers(handlers => { #if IOS handlers.AddHandler(); #endif }); ``` ## Wrap up In this post, I show the code I used when you add a `ListView` in your project and an item highlighted in MAUI, its background is in orange. I hope this code can help you. Please let me know your thoughts in the comments or the [forum](https://puresourcecode.com/forum/). **Categories:** .NET8, MAUI **Tags:** listview, maui, net8 **Hashtags:** maui, net8 --- ### [Create TabBar in MAUI](https://puresourcecode.com/dotnet/maui/create-tabbar-in-maui/) **Published:** February 2, 2024 **Author:** Enrico **Excerpt:** I show you how to create a nice #TabBar in #MAUI without using any external NuGet package or components. Fully customizable and 100% #XAML. **Content:** In this post, I show you how to create a nice TabBar in [MAUI](https://puresourcecode.com/tag/maui/) without using any external NuGet package or components. Fully customizable and 100% [XAML](https://puresourcecode.com/?s=xaml). Here is the result of the code: ![MAUI TabBar in Windows - Create TabBar in MAUI](https://github.com/erossini/MauiTabs/assets/9497415/6885f87b-211e-44a4-b832-99f0586d8d6f)MAUI TabBar in Windows ![MAUI Tabs in Android - Create TabBar in MAUI](https://github.com/erossini/MauiTabs/assets/9497415/84521ba4-48d7-4aa0-af77-2125ffe44ad1)MAUI TabBar in Android ![MAUI Tabs in iOS - Create TabBar in MAUI](https://github.com/erossini/MauiTabs/assets/9497415/a9d1c721-6b48-45b9-b52c-317648c76f63)MAUI Tabs in iOS The source code of this post is on [GitHub](https://github.com/erossini/MauiTabs). ## Bindable Layout .NET MAUI bindable layouts enable any layout class that derives from the [Layout](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.layout) class to generate its content by binding to a collection of items, with the option to set the appearance of each item with a [DataTemplate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.datatemplate). Bindable layouts are provided by the [BindableLayout](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindablelayout) class, which exposes the following attached properties: - `ItemsSource` – specifies the collection of `IEnumerable` items to be displayed by the layout. - `ItemTemplate` – specifies the [DataTemplate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.datatemplate) to apply to each item in the collection of items displayed by the layout. - `ItemTemplateSelector` – specifies the [DataTemplateSelector](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.datatemplateselector) that will be used to choose a [DataTemplate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.datatemplate) for an item at runtime. - In addition, the [BindableLayout](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindablelayout) class exposes the following bindable properties: - `EmptyView` – specifies the `string` or view that will be displayed when the `ItemsSource` property is `null`, or when the collection specified by the `ItemsSource` property is `null` or empty. The default value is `null`. - `EmptyViewTemplate` – specifies the [DataTemplate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.datatemplate) that will be displayed when the `ItemsSource` property is `null`, or when the collection specified by the `ItemsSource` property is `null` or empty. The default value is `null`. ### Populate a bindable layout with data A bindable layout is populated with data by setting its `ItemsSource` property to any collection that implements `IEnumerable`, and attaching it to a [Layout](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.layout)-derived class: ``` ``` When the `BindableLayout.ItemsSource` attached property is set on a layout, but the `BindableLayout.ItemTemplate` attached property isn’t set, every item in the `IEnumerable` collection will be displayed by a [Label](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.label) that’s created by the [BindableLayout](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindablelayout) class. ## Radiobutton The .NET Multi-platform App UI (.NET MAUI) [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) is a type of button that allows users to select one option from a set. Each option is represented by one radio button, and you can only select one radio button in a group. By default, each [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) displays text: ![Screenshot of RadioButtons.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/radiobuttons-default.pngviewnet-maui-8.png?w=640&ssl=1) However, on some platforms a [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) can display a [View](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.view), and on all platforms the appearance of each [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) can be redefined with a [ControlTemplate](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.controltemplate): ![Screenshot of re-defined RadioButtons.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/02/radiobuttons-controltemplate.pngviewnet-maui-8.png?w=640&ssl=1) [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) defines the following properties: - `Content`, of type `object`, which defines the `string` or [View](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.view) to be displayed by the [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton). - `IsChecked`, of type `bool`, which defines whether the [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) is checked. This property uses a `TwoWay` binding, and has a default value of `false`. - `GroupName`, of type `string`, which defines the name that specifies which [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) controls are mutually exclusive. This property has a default value of `null`. - `Value`, of type `object`, which defines an optional unique value associated with the [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton). - `BorderColor`, of type [Color](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.graphics.color), which defines the border stroke color. - `BorderWidth`, of type `double`, which defines the width of the [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) border. - `CharacterSpacing`, of type `double`, which defines the spacing between characters of any displayed text. - `CornerRadius`, of type `int`, which defines the corner radius of the [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton). - `FontAttributes`, of type `FontAttributes`, which determines text style. - `FontAutoScalingEnabled`, of type `bool`, which defines whether an app’s UI reflects text scaling preferences set in the operating system. The default value of this property is `true`. - `FontFamily`, of type `string`, which defines the font family. - `FontSize`, of type `double`, which defines the font size. - `TextColor`, of type [Color](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.graphics.color), which defines the color of any displayed text. - `TextTransform`, of type `TextTransform`, which defines the casing of any displayed text. These properties are backed by [BindableProperty](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.bindableproperty) objects, which means that they can be targets of data bindings, and styled. [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) also defines a `CheckedChanged` event that’s raised when the `IsChecked` property changes, either through user or programmatic manipulation. The `CheckedChangedEventArgs` object that accompanies the `CheckedChanged` event has a single property named `Value`, of type `bool`. When the event is raised, the value of the `CheckedChangedEventArgs.Value` property is set to the new value of the `IsChecked` property. [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) grouping can be managed by the `RadioButtonGroup` class, which defines the following attached properties: - `GroupName`, of type `string`, which defines the group name for [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) objects in an `ILayout`. - `SelectedValue`, of type `object`, which represents the value of the checked [RadioButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.maui.controls.radiobutton) object within an `ILayout` group. This attached property uses a `TwoWay` binding by default. For more information about the `GroupName` attached property, see [Group RadioButtons](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/radiobutton?view=net-maui-8.0#group-radiobuttons). For more information about the `SelectedValue` attached property, see [Respond to RadioButton state changes](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/controls/radiobutton?view=net-maui-8.0#respond-to-radiobutton-state-changes). ## The UI implementation First, I created my container with a set of data to be repeated for each tab. ``` Hot Dishes Cold Dishes Soups Appetizers Desserts ``` Then, I add the `RadioButton` ``` ``` To make this a grouped list of radios, I added a name to the parent layout. ``` ``` ### Add a style Then, I styled the `RadioButton` using a `ControlTemplate`. ``` ``` Because we are inside of a control template, rather than using `Binding` I use `TemplateBinding`. The `Content` could be anything, but I supplied a `String` so it seems safe to bind that directly to the label text. Now, to get a different look for selected vs unselected, I: - added a `VisualStateManager` (VSM) to the control template layout - gave my label and box names so I could target them from the VSM - styled the checked and unchecked states ``` ``` ## Binding with a ViewModel Now, I have a nice and easy way to display tabs but I want to bind the UI with my data that are coming from the ViewModel. First, I add a model for the items called `MenuItem` ``` using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace MauiTabs { /// /// Class MenuItem. /// public class MenuItem { /// /// Gets or sets the text. /// /// The text. public string? Text { get; set; } /// /// Gets or sets the value. /// /// The value. public string? Value { get; set; } } } ``` ### Create the ViewModel Now, using the `CommunityToolkit` I’m going to create the ViewModel as in the following code: ``` using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; namespace MauiTabs { public partial class MainPageViewModel : ObservableObject { [ObservableProperty] private List? _menuItems; [ObservableProperty] private string? _selected; public MainPageViewModel() { MenuItems = new List { new MenuItem { Text = "Words", Value = "Words" }, new MenuItem { Text = "Games", Value = "Games" }, new MenuItem { Text = "Statistics", Value = "Statistics" } }; Selected = MenuItems[0].Value; } } } ``` In line 13, I define the selected or default value. In this variable, the application will save the choice of the user. It is a simple `string` that will contain the `Value` of the `MenuItem`. Then, in line 24, I define by default that the default element alias the element I want to highlight when the page will appear is the first one. ### Bind the ViewModel Now, in the page, in my case \_MainPage\_, I’m going to bind the ViewModel to the page itself like that: ``` namespace MauiTabs { public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); BindingContext = new MainPageViewModel(); } } } ``` ### Change the XAML After everything, I have to change the XAML to display the text and the value for the ViewModel. ``` ``` In line 12, I bind the variable `Selected` to the `RadioButtonGroup.SelectedValue`. The first time the page is displayed, the first `RadioButton` is selected as expected. When a user taps on the others, the `Selected` value will change with the defined `Value`. In lines 18 and 20, I bind the `Text` to display and the Value for each element. Remember to set the `GroupName` because instead it will not work. Finally, in line 57, the `Selected` value will be display. ## Wrap up In conclusion, I hope this code will help you to create a nice TabBar in MAUI without using third-party components and full-customized as you like. Let me know what you think in the comment or in the [Forum](https://puresourcecode.com/forum/). **Categories:** .NET8, MAUI **Tags:** maui, net8, tabs **Hashtags:** maui, net8 --- ### [Some lessons I learned about MAUI](https://puresourcecode.com/dotnet/maui/some-lessons-i-learned-about-maui/) **Published:** January 31, 2024 **Author:** Enrico **Excerpt:** I want to share with you some lessons I learned about MAUI. In the last few months, I have been creating an application using NET8 MAUI. **Content:** In this post, I want to share with you some lessons I learned about MAUI. In the last few months, I have been creating an application using [NET8](https://puresourcecode.com/category/dotnet/net8/) [MAUI](https://puresourcecode.com/tag/maui/). I faced a lot of issues and step-by-step I resolved most of them. ## OnDisappearing This method is called when the page disappears due to navigating away from the page within the app. It is not called when the app disappears due to an event external to the app (e.g., the user navigates to the home screen or another app, a phone call is received, the device is locked, and the device is turned off). Like in the old Xamarin, I added this method ``` protected override void OnDisappearing() { Shell.Current.GoToAsync("..", true); return base.OnBackButtonPressed(); } ``` With this code, the application goes back of 2 pages: one page from the system and another one because of the code. So, be careful what adding in this function. ## Platform-specific XAML Sometimes, I have to determine the behaviours of the application based on the platform. For example, if the application is running on Windows, I want to display a `ContextMenu`. For this reason, have to know the platform. Here, we have a *VerticalStackLayout* and it has an `x:Name` set so that we can manipulate the contents of it by accessing it via the `VerticalLayout` identifier in the code-behind. ``` ``` ### Runtime decision The simplest way to show platform-specific views is to make the appropriate calls in the code-behind based on the current runtime platform: ``` if (DeviceInfo.Platform == DevicePlatform.Android) { VerticalLayout.Add(new Android.ViewAndroid()); } else if (DeviceInfo.Platform == DevicePlatform.iOS) { VerticalLayout.Add(new iOS.ViewiOS()); } else if (DeviceInfo.Platform == DevicePlatform.macOS) { VerticalLayout.Add(new iOS.ViewMacCatalyst()); } else if (DeviceInfo.Platform == DevicePlatform.WinUI) { VerticalLayout.Add(new iOS.ViewWindows()); } ``` The downside of this approach is that the resulting code will be available on all target platforms. ### Conditional compilation An alternative and slightly better approach compared to the runtime decision is to use conditional compilation instead: ``` #if ANDROID VerticalLayout.Add(new Android.ViewAndroid()); #elif IOS VerticalLayout.Add(new iOS.ViewiOS()); #elif MACCATALYST VerticalLayout.Add(new MacCatalyst.ViewMacCatalyst()); #elif WINDOWS VerticalLayout.Add(new Windows.ViewWindows()); #endif ``` The advantage of this is that only the appropriate calls end up in the compiled app code for each target platform and no decision needs to be taken during runtime. ### XAML-only approach It’s also possible to show only the relevant parts of the UI based on the current runtime platform by only using XAML and no C# code by taking advantage of ``: ``` ``` Here, we have a *ContentView* that serves as a container and we control its content by using the `` class and providing different views for each platform. It’s important to include the `x:TypeArguments="View"` attribute, because we need to tell `` what the return type is as the `ContentView` class only accepts `View` instances as its `Content`. **Note:** *This is equivalent to the runtime decision approach in the code-behind, meaning that all views of all platforms will be included in the app bundle.* It’s also possible to use `` to provide different views depending on the device type *(e.g. tablet, phone, desktop)*: ``` ``` ## MAUI and iOS: icon set or app icon set named “appicon” error I would like to test it on an iOS simulator. When I try to run the application, I get this error > None of the input catalogs contained a matching stickers icon set or app icon set named “appicon”. After binging a bit, I found this post on [GitHub](https://github.com/dotnet/maui/issues/14721) where they say: > In the info.plist you have > > XSAppIconAssets Assets.xcassets/appicon.appiconset The AppIcon file name is icon.svg > > What worked for me was making sure those name matched, ex rename the svg to appicon.svg or in info.plist to icon.appiconset or > I had the same problem what fixed it for me was renaming my icon file from “orpheus\_icon.png” to “orpheus.png” and changing this line in the info.plist to Assets.xcassets/orpheus.appiconset Those answers were related to the NET7 and the previous version of MAUI. I think something has changed since then. In my project, I have the folder **Platform > iOS > AppIcon.appiconset** and the autogenerated images. In **Resources**, I have the *appicon.svg*. [![enter image description here](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/BavBb.png?w=640&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/BavBb.png?ssl=1) In the *Info.plist*, I have ``` XSAppIconAssets Assets.xcassets/appicon.appiconset ``` ### Solution Removing the `bin` and `obj` folders is not helping. The solution I found is to delete the `Assets.appiconset` from the `iOS` folder. ## SecureStorage in Windows raises an error if saves an empty value In my `NET8` `MAUI` app, I’m using `SecureStorage` to save the user’s username. In Android, the function is working. I call the API to authenticate the user and as response, I get the *UserId* and *UserName*. Sometimes, the *UserId* is empty. [![enter image description here](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/xC1gT.png?w=640&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/xC1gT.png?ssl=1) When I save the empty *UserId* with this code in Windows (working for Android) ``` await SecureStorage.Default.SetAsync("userId", login.UserId); await SecureStorage.Default.SetAsync("username", login.UserName); ``` the app raises this error > System.ArgumentException: ‘Value does not fall within the expected range.’ [![enter image description here](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/3N2sO.png?w=640&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/3N2sO.png?ssl=1)### Issue This exception is coming from the implementation of the [SecureStorage for Windows](https://github.com/dotnet/maui/blob/6295ed8aba7b62fdb5014fd30f46f38f885b9dcd/src/Essentials/src/SecureStorage/SecureStorage.uwp.cs#L39). It was caused by `var buffer = await provider.ProtectAsync(bytes.AsBuffer()); `. This should be a limit of the Windows native API. The null will also throw an exception about the value can’t be null. So the string.Empty and null are not supported for the SecureStorage in the maui on the Windows platform. ### Solution I have to check if the value is not null and save only if it is not null. To remove a key use ``` SecureStorage.Default.Remove("userId"); ``` and don’t try to add an empty string. ## AppCenter crashes iOS application In all my apps, I usually add AppCenter to track the events and the crashes. For this reason, I add the packages `Microsoft.AppCenter.Analytics` and `Microsoft.AppCenter.Crashes packages` to my projects. The issue is that when I try to deploy the application to an iOS Simulator I get this error: > clang++ exited with code 1: ld: in /Users/enrico/Library/Caches/Xamarin/mtbs/builds/LanguageInUse/1fa03704bb15e35c6f47a701d9d92131e3e0740198296a93338bb3c829bc9cf7/obj/Debug/net8.0-ios/iossimulator-arm64/linker-cache/AppCenterCrashes.a(MSACErrorReport.o), building for iOS Simulator, but linking in object file built for iOS, file ‘/Users/enrico/Library/Caches/Xamarin/mtbs/builds/LanguageInUse/1fa03704bb15e35c6f47a701d9d92131e3e0740198296a93338bb3c829bc9cf7/obj/Debug/net8.0-ios/iossimulator-arm64/linker-cache/AppCenterCrashes.a’ clang: error: linker command failed with exit code 1 (use -v to see invocation) LanguageInUse C:\\Program Files\\dotnet\\packs\\Microsoft.iOS.Sdk\\17.2.8004\\targets\\Xamarin.Shared.Sdk.targets 1559 Also, I tried different `Target iOS Framework` but I get the same result. ### Solution The underlying issue seems to be that the AppCenter NuGet doesn’t support the ARM64 architecture in the simulator. As shared in [this MAUI GitHub issue](https://github.com/dotnet/maui/issues/16778), you have to enter this in your CSPROJ file: ``` true ``` ## Orange selected ListView item Here is an issue with MAUI that made me crazy for almost 2 months. I added a `ListView` on my page and displayed some data. When I tap on an item, the background color is orange. ![Example of ListView - Some lessons I learned about MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/woliT.gif?resize=629%2C945&ssl=1)Example of ListView This is a well-known issue in MAUI (see the [issue on GitHub](https://github.com/dotnet/maui/issues/13812)). To fix this issue, I had to create a custom `ViewCell` for each platform. Because the solution is quite long, I created a post for it. Jump on [Orange selected ListView item highlighted in MAUI](https://puresourcecode.com/dotnet/maui/orange-selected-listview-item-highlighted-in-maui/) to see the full implementation. ## Publish an Android app warnings for deobfuscation and native code When I upload an app bundle or `apk` on the `Google Play console`, I get a warning > > There is no deobfuscation file associated with this App Bundle. If you use obfuscated code (R8/proguard), uploading a deobfuscation file will make crashes and ANRs easier to analyse and debug. Using R8/proguard can help reduce app size. > > > This App Bundle contains native code, and you’ve not uploaded debug symbols. We recommend that you upload a symbol file to make your crashes and ANRs easier to analyse and debug. > > > > Google Play console ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/image-3.png?resize=640%2C262&ssl=1)So, the solution is to open the **Properties** of the project and then select the option for **Android**. There is a setting for **R8 code shrinker**. Check this option. ![Android R8 code shrinker option - Some lessons I learned about MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/image-4.png?resize=640%2C309&ssl=1)Android R8 code shrinker option **Categories:** .NET8, MAUI **Tags:** maui, net8 **Hashtags:** maui, net8 --- ### [Excel export Json for Azure Active Directory](https://puresourcecode.com/tools/microsoft-office/excel-export-json-for-azure-active-directory/) **Published:** January 22, 2024 **Author:** Enrico **Excerpt:** A VBA script converts Excel tables to JSON and exports the data to a file, for App Registration for Azure Active Directory **Content:** A VBA script that converts Excel tables to JSON format and exports the data to a file at the location of your choice, in particular for `Groups` and `AppRoles` for `Azure Active Directory`. You have the full code on [GitHub](https://github.com/erossini/ExcelExportJsonForAAD). ![Visual Basic for Application with the project - Excel export Json for Azure Active Directory](https://github.com/erossini/ExcelExportJsonForAAD/assets/9497415/480b40c8-cc85-4c1e-92f2-fcd8f59d41fb)Visual Basic for Application with the project ### Installation You can use this script by following these steps: 1. Open up Microsoft Excel 2. Go to the **Developer** tab (For more information on how to show the developer tab, go [here](https://support.office.com/en-us/article/show-the-developer-tab-e1192344-5e56-4d45-931b-e5fd9bea2d45?omkt=en-001&ui=en-US&rs=en-001&ad=US)) 3. Click on **Visual Basic**, in the upper left corner of the window 4. In the toolbar at the top of the window that appears, click on **File** > **Import file…** 5. Select **ExcelToJSON.bas** and click on **Open** 6. Click on **File** > **Import file…** for a second time 7. Select **ExcelToJSONForm.frm** and click on **Open** (make sure that **ExcelToJSONForm.frx** is located in the same folder, or this step will not work) ### Usage To use the script, you need an Excel file with at least one table in it. Once you do, follow these instructions: 1. Go to the **Developer** tab 2. Click on **Macros** 3. Select **yourfile.XLSB!ExcelToJSON.ExcelToJSON** 4. Click on **Run** 5. In the window that appears, select which tables that you would like to export, and then click on **Submit** 6. Finally, select the name for the JSON file that will be selected as well as the location that you would like to save the file in ## Example In an Excel file, you map the `Groups` for `Azure Active Directory` that you want to create or associate. For example, you have a table like that. GroupNameAppRegAppRoles{ENV}\_Contributors{ENV}\_APIDesigner{ENV}\_Contributors{ENV}\_APIEditor{ENV}\_Contributors{ENV}\_APITeam\_Users{ENV}\_Contributors{ENV}\_APIViewer{ENV}\_Contributors{ENV}\_UIDesigner{ENV}\_Contributors{ENV}\_UIEditor{ENV}\_Contributors{ENV}\_UITeam\_Users{ENV}\_Contributors{ENV}\_UIViewer{ENV}\_Contributors{ENV}\_UIAdminDesigner{ENV}\_Contributors{ENV}\_UIAdminEditor{ENV}\_Contributors{ENV}\_UIAdminViewer{ENV}\_Contributors{ENV}\_API2\_APITeam\_Users{ENV}\_Contributors{ENV}\_API2\_APIAdminDesigner{ENV}\_Contributors{ENV}\_API2\_APIAdminEditor{ENV}\_Contributors{ENV}\_API2\_APIAdminViewer{ENV}\_Dev\_Leads{ENV}\_APIAdmin{ENV}\_Dev\_Leads{ENV}\_APIDesigner{ENV}\_Dev\_Leads{ENV}\_APIEditor{ENV}\_Dev\_Leads{ENV}\_APIExporter{ENV}\_Dev\_Leads{ENV}\_APIImporter{ENV}\_Dev\_Leads{ENV}\_APITeam\_Users{ENV}\_Dev\_Leads{ENV}\_APIViewer{ENV}\_Dev\_Leads{ENV}\_UIAdmin{ENV}\_Dev\_Leads{ENV}\_UIDesigner{ENV}\_Dev\_Leads{ENV}\_UIEditor{ENV}\_Dev\_Leads{ENV}\_UIExporter{ENV}\_Dev\_Leads{ENV}\_UIImporter{ENV}\_Dev\_Leads{ENV}\_UITeam\_Users{ENV}\_Dev\_Leads{ENV}\_UIViewer{ENV}\_Dev\_Leads{ENV}\_UIAdminAdmin{ENV}\_Dev\_Leads{ENV}\_UIAdminDesigner{ENV}\_Dev\_Leads{ENV}\_UIAdminEditor{ENV}\_Dev\_Leads{ENV}\_UIAdminExporter{ENV}\_Dev\_Leads{ENV}\_UIAdminImporter{ENV}\_Dev\_Leads{ENV}\_UIAdminViewerNow, the issue is how to create a Json file for this table. There is an export in Excel that creates a Json but not in the format that is required for the Active Directory. By the way, the expected `json` is like the following one ``` { "Groups": [ { "GroupName": "{ENV}_Contributors", "AppRegs": [ { "AppRegName": "{ENV}_API", "AppRoles": [ "Designer", "Editor", "Team_Users", "Viewer" ] }, { "AppRegName": "{ENV}_UI", "AppRoles": [ "Designer", "Editor", "Team_Users", "Viewer", "AdminDesigner", "AdminEditor", "AdminViewer" ] }, { "AppRegName": "{ENV}_API2_API", "AppRoles": [ "Team_Users", "AdminDesigner", "AdminEditor", "AdminViewer" ] } ] }, { "GroupName": "{ENV}_Dev_Leads", "AppRegs": [ { "AppRegName": "{ENV}_API", "AppRoles": [ "Admin", "Designer", "Editor", "Exporter", "Importer", "Team_Users", "Viewer", "TRSCore" ] }, { "AppRegName": "{ENV}_UI", "AppRoles": [ "Admin", "Designer", "Editor", "Exporter", "Importer", "Team_Users", "Viewer", "AdminAdmin", "AdminDesigner", "AdminEditor", "AdminExporter", "AdminImporter", "AdminViewer", "TRSCore" ] }, { "AppRegName": "{ENV}_API2_API", "AppRoles": [ "Team_Users", "AdminAdmin", "AdminDesigner", "AdminEditor", "AdminExporter", "AdminImporter", "AdminViewer", "TRSCore" ] } ] }, { "GroupName": "{ENV}_DevOps", "AppRegs": [ { "AppRegName": "{ENV}_API", "AppRoles": [ "Admin", "Designer", "Editor", "Exporter", "Importer", "Team_Users", "Viewer" ] }, { "AppRegName": "{ENV}_UI", "AppRoles": [ "Admin", "Designer", "Editor", "Exporter", "Importer", "Team_Users", "Viewer", "AdminAdmin", "AdminDesigner", "AdminEditor", "AdminExporter", "AdminImporter", "AdminViewer" ] }, { "AppRegName": "{ENV}_API2_API", "AppRoles": [ "Team_Users", "AdminAdmin", "AdminDesigner", "AdminEditor", "AdminExporter", "AdminImporter", "AdminViewer" ] } ] }, { "GroupName": "{ENV}_Internal_Client_Support", "AppRegs": [ { "AppRegName": "{ENV}_API", "AppRoles": [ "Designer", "Editor", "Team_Users", "Viewer", "TRSCore" ] }, { "AppRegName": "{ENV}_UI", "AppRoles": [ "Designer", "Editor", "Team_Users", "Viewer", "AdminDesigner", "AdminEditor", "AdminViewer", "TRSCore" ] }, { "AppRegName": "{ENV}_API2_API", "AppRoles": [ "Team_Users", "AdminDesigner", "AdminEditor", "AdminViewer", "TRSCore" ] } ] }, { "GroupName": "{ENV}_Users", "AppRegs": [ { "AppRegName": "{ENV}_API", "AppRoles": [ "Editor", "Team_Users", "Viewer" ] }, { "AppRegName": "{ENV}_UI", "AppRoles": [ "Editor", "Team_Users", "Viewer", "AdminEditor", "AdminViewer" ] }, { "AppRegName": "{ENV}_API2_API", "AppRoles": [ "Team_Users", "AdminEditor", "AdminViewer" ] } ] } ] } ``` Because this structure is a little complex, I have to create something my own export. With this code, when I run the `Macro`, I get a window with the list of the tables in the spreadsheet. ![Example of a generated window with the list of tables - Excel export Json for Azure Active Directory](https://github.com/erossini/ExcelExportJsonForAAD/assets/9497415/2dda0ff4-40bf-429a-b6d1-306fbfb14b5e)Example of a generated window with the list of tables Then, I can select one or more tables I want to export. Remember this script generates only one `json` file. After that, I have to choose the location and the name of the file I want to create. ## The form The first part of Excel export Json for Azure Active Directory is to create a simple form to select the list of tables in the spreadsheet. So, the user can select one or more from the list in order to export into a json file. The form is in the [GitHub](https://github.com/erossini/ExcelExportJsonForAAD) repository. There are 2 files: `ExcelToJSONForm.frm` and `ExcelToJSONForm.frx`. When in the `Macro` you import the first file, automatically, both files are imported in the spreadsheet. When you run the macro, the form is shown and it displays the list of tables in the all Excel file. Select the table you want to export and then click **Submit**. This executes this code ``` Private Sub SubmitBtn_Click() Dim numCheckedBoxes As Integer For Each userFormControl In ExcelToJSONForm.Controls If TypeName(userFormControl) = "CheckBox" Then If userFormControl = True Then numCheckedBoxes = numCheckedBoxes + 1 End If End If Next userFormControl If numCheckedBoxes = 0 Then MsgBox "Please select one or more tables before proceeding" Else j = 0 ReDim Preserve usrSlctdTblsNameArray(0 To numCheckedBoxes) For Each userFormControl In ExcelToJSONForm.Controls If TypeName(userFormControl) = "CheckBox" Then If userFormControl = True Then j = j + 1 usrSlctdTblsNameArray(j) = userFormControl.Caption End If End If Next userFormControl Me.Hide End If End Sub ``` In the line 18, the variable `usrSlctdTblsNameArray` is re-redimensioned based on the number of checkboxes that are checked. This variable is defined in the `ExcelToJSON.bas` file. ## The code Now, everything is happening in the `Sub ExcelToJSON()`. This function opens the form and adds the checkboxes, one for each table in the spreadsheet. ### The logic What is the logic behind the generation of the file? As you see in the `json` file, there are 3 levels: - **GroupName** is the name of the group. This group has one or more App Registration - **AppReg** is the name of the application to register in the Active Directory. This has in attachment a list of AppRoles - **AppRoles** is the list of roles related to a specific application. This is a list of string So, the code reads line by line the selected tables and organizes the `json` accordingly. ## Wrap up In conclusion, this is how I created an Excel export Json for Azure Active Directory. Let me know if the code is clear enough. Please give me your feedback or open a post in the [forum](https://puresourcecode.com/forum/). **Categories:** Microsoft Office **Tags:** active-directory, microsoft-excel, vba --- ### [Open a loading popup from MAUI viewmodel](https://puresourcecode.com/dotnet/maui/open-a-loading-popup-from-maui-viewmodel/) **Published:** January 8, 2024 **Author:** Enrico **Excerpt:** Here I show how to open a loading popup from ViewModel in MAUI. It is quite an easy implementation, but it is working quite well. **Content:** In this new post, I show how to open a loading popup from ViewModel in [MAUI](https://puresourcecode.com/category/dotnet/maui/). It is quite an easy implementation but it is working quite well. ## Add CommunityToolkit.Maui Create a new .NET MAUI project. Once that’s done, install the NuGet package **CommunityToolkit.Maui** into your project, as shown in the screenshot below: ![.NET MAUI Community Toolkit in the NuGet Package Manager - Open a loading popup from MAUI viewmodel](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/image-2.png?resize=640%2C164&ssl=1).NET MAUI Community Toolkit in the NuGet Package Manager Add the necessary initialization code in order to use the toolkit. Now we can move onto creating the actual popup. ## Create the popup The .NET MAUI Community Toolkit provides the [Popup](https://docs.microsoft.com/en-us/dotnet/communitytoolkit/maui/views/popup) view, which you can use to create a custom UI that you can present to your users. We’ll use this and populate it with an `ActivityIndicator`, which will be our spinner. Right-click your project and select Add New Item -> .NET MAUI -> .NET MAUI ContentPage (XAML). Name it `LoadingPopup.xaml`: ![New .NET MAUI ContentPage (XAML) from the Add New Item dialog.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2024/01/image-1.png?resize=640%2C444&ssl=1)New .NET MAUI ContentPage (XAML) from the Add New Item dialog. Replace the XAML content with this: ``` ``` And update the code-behind (`LoadingPopup.xaml.cs`) to now inherit from `Popup` instead of `ContentPage`: ``` public partial class LoadingPopup : Popup { ... } ``` ### Open the popup Since the project template already has a button in the `MainPage` file, we can update the `Clicked` event handler to open our popup. Go to the code-behind file of `MainPage.xaml.cs` and replace the content of the `OnCounterClicked` method: ``` var popup = new LoadingPopup(); this.ShowPopup(popup); ``` That’s it! That’s all you need to create your own spinner popup. If you want to programatically close the popup, you can call the `Close()`-method on the popup. Also, by default your popups can be dismissed by clicking outside of it. If you want to prevent this, you can set the `CanBeDismissedByTappingOutsideOfPopup` property of the Popup to `false` in your XAML. ## Popup Service Now, from the ViewModels, I can’t call the function `ShowPopup` or close an open popup. For this reason, I must create a service that helps me display or hide a popup from the ViewModel. ### IPopupService For this reason, the first step is to create an `IPopupService` interface ``` namespace MyProject.Interfaces { public interface IPopupService { void Init(Page page); void ClosePopup(Popup popup); void ShowPopup(Popup popup); } } ``` This is a very simple interface but it has everything I need to display and hide a popup. ### PopupService Now, the next step is to implement this service in the project. The code is following ``` public class PopupService : IPopupService { Page page { get; set; } public void ClosePopup(Popup popup) { if (page == null) page = Application.Current?.MainPage ?? throw new NullReferenceException(); popup.Close(); } public void Init(Page page) { this.page = page; } public void ShowPopup(Popup popup) { if (page == null) page = Application.Current?.MainPage ?? throw new NullReferenceException(); page.ShowPopup(popup); } } ``` ### Register the service After that, I have to register this service in the \_MauiProgram.cs\_. So, place this line in it: ``` builder.Services.AddTransient(); ``` As you can see, I added `Interfaces` because it could be confusion between `IPopupService` from the **CommunityToolkit.Maui** and my implementation. ## Use the service Now, on your page or in your view model, it is enough to inject this service in the constructor. For example: ``` Interfaces.IPopupService popupService; Page page; public MyViewModel(IPopupService popupService) { this.popupService = popupService; } ``` ### Open the popup After that, in the view model, I can invoke the `LoadingPage` to popup like ``` var loading = new LoadingPopup(); popupService.ShowPopup(loading); ``` ### Close the popup Now, to close the popup is enough to invoke the `Close` method like ``` popupService.ClosePopup(loading); ``` ## Wrap up In this post, I show you how to open a loading popup from MAUI ViewModel using **CommunityToolkit.Maui** and an implemented custom service. **Categories:** .NET8, MAUI **Tags:** maui, net8 **Hashtags:** maui, net8 --- ### [NET8, Blazor and Custom User Management](https://puresourcecode.com/dotnet/csharp/net8-blazor-and-custom-user-management/) **Published:** October 25, 2023 **Author:** Enrico **Excerpt:** I will show how to create custom user management with NET8 and Blazor based on Microsoft Identity. Here is how to add custom fields for users **Content:** I will show how to create custom user management with NET8 and Blazor based on Microsoft Identity. Here is how to add custom fields for users In every application I wrote, there is always the requirement to authenticate the user: in this new series of posts, I will show how to create custom user management with [NET8](https://puresourcecode.com/dotnet/net8/net8-is-announced/) and [Blazor](https://puresourcecode.com/tag/blazor/) based on [Microsoft Identity](https://puresourcecode.com/tag/microsoft-identity). The full source code of this post is available on [GitHub](https://github.com/erossini/NET8BlazorIdentity). If you find this post useful, please consider making a donation on [GitHub](https://github.com/sponsors/erossini). For any comment, suggestion or help, please see the [Forum](https://puresourcecode.com/forum/). Other posts: - [Custom User Management with NET8 and Blazor](https://puresourcecode.com/dotnet/blazor/custom-user-management-with-net8-and-blazor/) (1st part) - [NET8, Blazor and Custom User Management](https://puresourcecode.com/dotnet/blazor/net8-blazor-and-custom-user-management/) (2nd part) ## Allow Login with both Username and Email In the first [post](https://puresourcecode.com/dotnet/blazor/custom-user-management-with-net8-and-blazor/), I showed how to add custom fields to the registration page. Ideally, I may want to allow my users to log in with both the username and the email ID. Under `Components \ Pages \ Account` open the Razor page `Login.razor`. Almost at the beginning of the `code` section you have the `InputModel`. Now, replace the `InputModel` with this one: ``` public class InputModel { [Required] [Display(Name = "Email / Username")] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } ``` At the model level, we made the Email Property accept both email IDs and plain text. Now, I add a new function to check if the entered data is a valid email ID or not. First, at the top of the page add ``` @using System.Net.Mail @inject UserManager UserManager ``` Then, at the bottom of the `code` section add this function to check is the email is a valid one: ``` public bool IsValidEmail(string emailaddress) { try { MailAddress m = new MailAddress(emailaddress); return true; } catch (FormatException) { return false; } } ``` Now, I have to check if the email is valid in order to find the username of the user. For this reason, I have to change the `LoginUser()` function adding a few lines at the beginning (full code for clarity): ``` public async Task LoginUser() { var userName = Input.Email; if (IsValidEmail(Input.Email)) { var user = await UserManager.FindByEmailAsync(Input.Email); if (user != null) { userName = user.UserName; } } // This doesn't count login failures towards account lockout // To enable password failures to trigger account lockout, set lockoutOnFailure: true var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); if (result.Succeeded) { Logger.LogInformation("User logged in."); RedirectManager.RedirectTo(ReturnUrl); } if (result.RequiresTwoFactor) { RedirectManager.RedirectTo( "/Account/LoginWith2fa", new() { ["ReturnUrl"] = ReturnUrl, ["RememberMe"] = Input.RememberMe }); } if (result.IsLockedOut) { Logger.LogWarning("User account locked out."); RedirectManager.RedirectTo("/Account/Lockout"); } else { errorMessage = "Error: Invalid login attempt."; } } ``` Build the application and run it. You would be able to log in with both the username and the email id now. ## Adding the Custom User Fields To Profile Settings If you click on the left side on your name, you are redirected to the **Manage your account** section. ![Manage your account - NET8, Blazor and Custom User Management](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-13.png?resize=640%2C373&ssl=1)Manage your account There are quite a lot of basic options here, like changing your phone number, updating the email id, changing the password, and so on. Let’s try to extend these pages in the coming sections. ### Change the InputModel As the first step, let’s try to add the first name and last name fields to this form. Navigate to `Components/Pages/Account/Manage/Index.razor`. Replace the `InputModel`. Here we added the new fields (including Profile Picture, although we will implement it in the next section) ``` private sealed class InputModel { [Display(Name = "First Name")] public string? FirstName { get; set; } [Display(Name = "Last Name")] public string? LastName { get; set; } [Display(Name = "Username")] public string? Username { get; set; } [Phone] [Display(Name = "Phone number")] public string? PhoneNumber { get; set; } [Display(Name = "Profile Picture")] public byte[]? ProfilePicture { get; set; } } ``` Next, while loading the form we need to load these data to the memory as well. So, change the `OnInitializedAsync` with the following code: ``` @code { private ApplicationUser _user = default!; private string? _firstname; private string? _lastname; private string? _username; private string? _phoneNumber; [SupplyParameterFromForm] private InputModel Input { get; set; } = default!; protected override async Task OnInitializedAsync() { Input ??= new(); _user = await UserAccessor.GetRequiredUserAsync(); _username = await UserManager.GetUserNameAsync(_user); _phoneNumber = await UserManager.GetPhoneNumberAsync(_user); _firstname = _user.FirstName; _lastname = _user.LastName; Input.FirstName ??= _firstname; Input.LastName ??= _lastname; Input.PhoneNumber ??= _phoneNumber; Input.ProfilePicture ??= _user.ProfilePicture; } ``` ### Change the UI The next step is to add the fields in the form. For that, in the `EditForm` tag add the following HTML code: ``` First name Last name ``` That’s it. Build and Run your application. Go to the “Manage your account” page. You will see the changes here. ![Your new profile page - NET8, Blazor and Custom User Management](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-14.png?resize=640%2C373&ssl=1)Your new profile page ## Adding a Profile Picture Remember the part where we added the Update code for FirstName and Lastname? Now, I want to do something similar for the picture profile of the user. There is a BUT. With NET8, Microsoft changes the lifecycle of a Blazor application. I recommend reading the article from [Microsoft Learn](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-7.0) to have a glance at the new changes. One of the exciting new entry is the `QuickGrid` component and you have more info [here](https://aspnet.github.io/quickgridsamples/). Before implementing the profile picture, there is one thing I want to highlight and it is the new `RenderMode.InteractiveServer`. ### Render modes So, every component in a Blazor Web App adopts a *render mode* to determine the hosting model that it uses, where it’s rendered, and whether or not it’s interactive. The following table shows the available render modes for rendering Razor components in a Blazor Web App. To apply a render mode to a component use the `@rendermode` directive on the component instance or on the component definition. Later in this article, examples are shown for each render mode scenario. NameDescriptionRender locationInteractiveStaticStatic server renderingServer❌NoInteractive ServerInteractive server rendering using Blazor ServerServer✔️YesInteractive WebAssemblyInteractive client rendering using Blazor WebAssemblyClient✔️YesInteractive AutoInteractive client rendering using Blazor Server initially and then WebAssembly on subsequent visits after the Blazor bundle is downloadedServer, then client✔️YesPrerendering is enabled by default for interactive components. Guidance on controlling prerendering is provided later in this article. The following examples demonstrate setting the component’s render mode with a few basic Razor component features. To test the render mode behaviours locally, you can place the following components in an app created from the *Blazor Web App* project template. When you create the app, select the checkboxes (Visual Studio) or apply the CLI options (.NET CLI) to enable both server-side and client-side interactivity. For guidance on how to create a Blazor Web App, see [Tooling for ASP.NET Core Blazor](https://learn.microsoft.com/en-us/aspnet/core/blazor/tooling?view=aspnetcore-8.0). ### Prepare the profile Now, in the `InputModel` I already added a field for the `ProfilePicture` to be saved in the database. In the `Index.razor` file, I have to accommodate the picture to be uploaded. So, I wrap the `EditForm` in a Bootstrap `col` and define 2 columns like that ``` // ... code omitted ... ``` As you can see, I added the `UploadProfilePicture`, a Razor component that has not been created yet. ### Implement the UploadProfilePicture component Now, the implementation of this Razor page should be quite straightforward. First, I have to add the code to display the image or ask the user to upload the image. ``` @if (User?.ProfilePicture?.Length > 0) { } else { } ``` As you can see, the image is coming from the database and then I convert the string from the database into a `Base64` string and then display the image. The code behind that is the following ``` @code { [Parameter] public ApplicationUser User { get; set; } = default!; [CascadingParameter] private Task AuthenticationState { get; set; } = default!; private async Task LoadFiles(InputFileChangeEventArgs e) { MemoryStream ms = new MemoryStream(); await e.File.OpenReadStream().CopyToAsync(ms); var bytes = ms.ToArray(); await using var scope = ServiceProvider.CreateAsyncScope(); var userManager = scope.ServiceProvider.GetRequiredService(); // Reload the ApplicationUser so we can make modifications to it in the new scope. var principal = (await AuthenticationState).User; User = await userManager.GetUserAsync(principal) ?? throw new Exception("Could not reload user!"); User.ProfilePicture = bytes; await userManager.UpdateAsync(User); } } ``` In order to have this code working, I have to add the following `using` ``` @using BlazorIdentity.Data @using Microsoft.AspNetCore.Identity @inject IServiceProvider ServiceProvider ``` Because this component has to interact with the server, I also have to add this new line at the top of the file ``` @rendermode RenderMode.InteractiveServer ``` This line informs Blazor that this component has to communicate with the server something. ## Wrap up So far, I explained how to improve the basic Identity section with a few changes to the original files. In the next section, I’m going to create a bunch of completely new files for managing users and roles. Then, I’m going to update the project to remove the concurrent access to the `UserManager`. Stay tuned! **Categories:** .NET8, Blazor, C# **Tags:** blazor, microsoft-identity, net8 **Hashtags:** blazor, net8 --- ### [Custom User Management with NET8 and Blazor](https://puresourcecode.com/dotnet/asp-net/custom-user-management-with-net8-and-blazor/) **Published:** October 24, 2023 **Author:** Enrico **Excerpt:** In this new series of posts, I will show how to create custom user management with NET8 and Blazor based on Microsoft Identity. **Content:** In this new series of posts, I will show how to create custom user management with NET8 and Blazor based on Microsoft Identity. In every application I wrote, there is always the requirement to authenticate the user: in this new series of posts, I will show how to create custom user management with [NET8](https://puresourcecode.com/dotnet/net8/net8-is-announced/) and [Blazor](https://puresourcecode.com/tag/blazor/) based on [Microsoft Identity](https://puresourcecode.com/tag/microsoft-identity). The full source code of this post is available on [GitHub](https://github.com/erossini/NET8BlazorIdentity). If you find this post useful, please consider making a donation on [GitHub](https://github.com/sponsors/erossini). For any comment, suggestion or help, please see the [Forum](https://puresourcecode.com/forum/). Other posts: - [Custom User Management with NET8 and Blazor](https://puresourcecode.com/dotnet/blazor/custom-user-management-with-net8-and-blazor/) (1st part) - [NET8, Blazor and Custom User Management](https://puresourcecode.com/dotnet/blazor/net8-blazor-and-custom-user-management/) (2nd part) ## Microsoft Identity: overview Every time we build an application, the first point of concern is how to manage the users and their roles and the security across the application. The basic essence of the requirement is always the same, which is to **register**, **login**, **authorize users**, **roles**, and so on. So, to help ease the user management process, Microsoft comes up with a default implementation of User Management. The name is **Microsoft Identity**, Also, it has built-in UI to support various user functionalities. Developers who are looking for a faster way to implement User Management, tend to go with Identity. You can learn more about Identity [here](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-3.1&tabs=visual-studio). Now, out of the box, Identity comes with certain basic features. In real scenarios, we may need much more than what Microsoft offers by default. This includes adding **Profile Pictures**, **UI for Role Management**, Custom logic to log in to the user, and much more. ## Setting up the Blazor Application At the time I’m writing with post, the only Visual Studio available is the version 2022 Preview and you can download it from [here](https://visualstudio.microsoft.com/vs/preview/). This is the only way to use [NET8](https://puresourcecode.com/dotnet/net8/net8-is-announced/). So, first **Create a new project**. From the list, choose **Blazor Web App**. ![Create a new project - Custom User Management with NET8 and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image.png?resize=640%2C426&ssl=1)Create a new project Give it a name and a location. ![Configure your new project - Custom User Management with NET8 and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-2.png?resize=640%2C426&ssl=1)Configure your new project In the **Addition information** step, from the dropdown list **Authentication Type**, select **Individual Accounts**. ![Additional Information - Custom User Management with NET8 and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-1.png?resize=640%2C426&ssl=1)Additional Information This is the result of the solution created by Visual Studio. How you can see, in the server project, under the `Pages` folder there is an `Account` folder with all the pages for the Identity. ![Blazor Identity solution - Custom User Management with NET8 and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-3.png?resize=393%2C931&ssl=1)Blazor Identity Solution ## Renaming the default Identity tables and updating Before moving on, let’s update the database. As soon as we created our project, Visual Studio has done the following for us already. - Added migrations for the Identity Table. - Generated a default DB Context - Registered the DB Context in the *Startup.cs* - Added a default connection string to *appsettings.json* (a local DB with the project name and GUID) Since everything is set up for us, let’s apply the migrations and update the database. Open up the package manager console and type in the following. ``` update-database ``` Once that’s done, open up the *SQL Server Object Explorer* in *Visual Studio*. You can see our newly generated Identity tables here. ![Identity Table](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-5.png?resize=352%2C418&ssl=1)Identity Tables Now, there is one thing that catches the eyes of many. The Table Names. Quite ugly with the ASPNET Naming convention, right? Let’s change that now. We will delete our new database for now. ``` drop-database ``` Also, delete the migrations folder (found inside the *Data* Folder), as we are going to generate a new one. Here is a simple solution. Since we are by default using **Entity Framework Core**, let’s open up the ApplicationDbContext.cs from the Data Folder. To modify the default ugly names of the Identity Tables, add this override function, ``` protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.HasDefaultSchema("Identity"); builder.Entity(entity => { entity.ToTable(name: "User"); }); builder.Entity(entity => { entity.ToTable(name: "Role"); }); builder.Entity(entity => { entity.ToTable("UserRoles"); }); builder.Entity(entity => { entity.ToTable("UserClaims"); }); builder.Entity(entity => { entity.ToTable("UserLogins"); }); builder.Entity(entity => { entity.ToTable("RoleClaims"); }); builder.Entity(entity => { entity.ToTable("UserTokens"); }); } ``` Line #4, sets a schema for the database. Line #7, renames the User Table from `ASPNETUsers` to `Identity.User`. Clean enough? Feel free to add tables names that can make more sense to you, Similarly, we rename all the table entries. With that out of the way, let’s add the migrations and update the database. ``` add-migration "Renamed Identity Table Names" update-database ``` Now, have a look at the database. What do you think? Is that much clearer now? ![Renamed Identity Table Names](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-6.png?resize=353%2C420&ssl=1)Renamed Identity Table Names ## Adding Custom Fields to Identity User If you go through the `Identity.User` Table, you can find over 10-15 columns that are available by default. What if we wanted to add certain user-specific properties like First Name, Image, and something else? For that, we have to need to extend the `IdentityUser` class with your own properties. In the `Data` folder, use can find an `ApplicationUser.cs` that inherits from the `IdentityUser`. ``` namespace LIU.Website.Data { // Add profile data for application users by adding properties to the ApplicationUser class public class ApplicationUser : IdentityUser { } } ``` So, in this class, I’m going to add a few properties to collect the following info about the new user: - first name - last name - number of changes to the username - profile picture The new `ApplicationUser` is ``` public class ApplicationUser : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } public int UsernameChangeLimit { get; set; } = 10; public byte[]? ProfilePicture { get; set; } } ``` Since we decided to change the default User class from IdentityUser to ApplicationUser, we would have to make other changes in our existing code as well. Now, add another migration ``` add-migration "Added Custom Properties" update-database ``` Now, if you open the table `Identity.AspNetUsers` we can see the changes to the table. ![Custom fields in the AspNetUsers table](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-7.png?resize=640%2C174&ssl=1)Custom fields in the AspNetUsers table ## Extending the Registration Form Now that we have added the extra fields, let’s use them in the registration process. Navigate to `Pages/Account/Register.razor`. The registration page is this one ![Registration page with Blazor NET8](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-8.png?resize=640%2C639&ssl=1)Registration page with Blazor NET8 What we have to do now is to change the model of this page to collect the first name and last name and then change the UI to display those values. ### Change InputModel So, in the code section of the page almost at the beginning, you find the `InputModel` that has all the default fields. Now, we are going to add the fields `FirstName` and `LastName`. The new `InputModel` is ``` public class InputModel { /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } = null!; /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } = null!; /// /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } = null!; [Required] [Display(Name = "First Name")] public string FirstName { get; set; } = null!; [Required] [Display(Name = "Last Name")] public string LastName { get; set; } = null!; } ``` Next, we will need to pass data to these properties and save it to the database while registering. If you scroll down in the page, there is a method `RegisterUser` that has as a parameter `EditContext editContext` (here the first lines) ``` public async Task RegisterUser(EditContext editContext) { var user = CreateUser(); await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); var emailStore = GetEmailStore(); await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); var result = await UserManager.CreateAsync(user, Input.Password); if (result.Succeeded) { ``` So, this method in the line `var user = CreateUser();` is calling the function `CreateUser`. I’m going to delete this line and change it with this code ``` MailAddress address = new MailAddress(Input.Email); string userName = address.User; var user = new ApplicationUser() { UserName = userName, Email = Input.Email, FirstName = Input.FirstName, LastName = Input.LastName }; ``` Remember to add `@using System.Net.Mail` at the top of the file. The code above creates the `Applicationser` after generating a username from the email address. For example, if the email address entered by the user is *info@puresourcecode.com*, the username generated will be *info*. ## Change the form Now, I have to display the fields in the registration form. For that, at the beginning of the page and after the `EditForm` I add the following HTML code ``` First name First name ``` Let’s check out the result. Build and run the application. Navigate to the Register Page. You can see the new fields now. Add some details to the form and try to register. ![The new registration form with custom fields](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-10.png?resize=640%2C512&ssl=1)The new registration form with custom fields Now, after the authentication, I can see this page. On the left side, the username (alias my email) is displayed. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-11.png?resize=640%2C266&ssl=1)Because I added the first name of a user, I want to display the first name if it is available or, instead the name from the Identity. So, open the `NavMenu.razor` and scroll down in the `AuthorizeView`. What I can see is ``` @context.User.Identity?.Name Logout ``` In order to display the first name, I have to change the line 5. First, at the top of the page add ``` // those 2 lines are related to the project @using BlazorIdentity.Components.Identity @using BlazorIdentity.Data @using Microsoft.AspNetCore.Identity @inject UserManager usermanager ``` The first 2 lines are related to my project. So, replace `BlazorIdentity` with the name of your project. In the last line, I inject the `UserManager` from the `Microsoft.AspNetCore.Identity`. Now, with this line I can write the new line ``` @(usermanager?.GetUserAsync(context.User)?.Result?.FirstName ?? context.User.Identity?.Name) Logout ``` Now, it is better! ![Read from the Identity a custom property](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/10/image-12.png?resize=640%2C279&ssl=1)Read from the Identity a custom property By default, in Identity, the username and Email are the same. Now, in the login form, the application expects the username in the email field. But our username is no longer an email ID, remember? Let’s fix this in the next post. **Categories:** .NET8, ASP.NET, Blazor **Tags:** blazor, identity, identityserver4, microsoft-identity **Hashtags:** blazor, net8 --- ### [Blazor integration with Identity Server](https://puresourcecode.com/tips-tricks/blazor-integration-with-identity-server/) **Published:** September 20, 2023 **Author:** Enrico **Excerpt:** I share the code for a Blazor integration with Identity Server and BFF. All browsers don't allow to share or save an authentication token **Content:** In this post, I share the code for a Blazor integration with Identity Server and BFF. Nowadays, all browsers don’t allow to share or save an authentication token. So, we have to find alternative ways to authenticate the users. In Duende Identity Server there is a new functionality called BFF. Unfortunately, for commercial use, we have to pay (see the first lines in the image below). ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/09/image.png?resize=640%2C353&ssl=1)The boilerplate of a Blazor project integrated with the Duende Identity Server is available on my [GitHub](https://github.com/erossini/BlazorDuendeConnection). If you want to read more about Identity Server and the integration with Blazor or ASP.NET, see those articles: - [Implement security workflow with Identity Server](https://puresourcecode.com/dotnet/net-core/implement-security-workflow-with-identity-server/) - [Authentication in ShinyProxy with IdentityServer](https://puresourcecode.com/programming-languages/r/authentication-in-shinyproxy-with-identityserver/) - Full list of posts [identityserver4 Archives](https://puresourcecode.com/tag/identityserver4/) ## What is XSS? A cross-site scripting (XSS) attack injects malicious code into vulnerable web applications. The idea behind XSS attacks is to execute malicious JavaScript in the user’s web browser. In general, XSS attacks are based on the victim’s trust in a legitimate website or vulnerable web application (the general XSS premises). When they succeed, the executed script can sniff the user’s cookies. If critical data is in the browser storage, it can be accessed. ## Stored (Persistent) Cross-Site Scripting attack This attack stores malicious scripts in the application server. The script can later be executed on the browser. It can affect more than one person. An example is the Twitter XSS attack. ## Reflected (Non-Persistent) Cross-Site Scripting attack The reflected XSS condition is met when a website or web application employs user input in HTML pages returned to the user’s browser, without validating the input first. With Non-Persistent cross-site scripting, malicious code is executed by the victim’s browser, and the payload is not stored anywhere; instead, it is returned as part of the response HTML that the server sends. ## DOM-based XSS DOM-based XSS vulnerabilities usually arise when JavaScript takes data from an attacker-controllable source, such as the URL, and passes it to a sink that supports dynamic code execution, such as eval() or innerHTML. Your website is vulnerable to this attack if any user’s inputs in the URL appear in the DOM via methods like innerHTML. This attack happens on the browser. It does not need a request/response cycle with the server to execute. Your application server is vulnerable if it doesn’t do the following: - If it doesn’t validate user request data. - If it doesn’t sanitize data stored on the database. - If it doesn’t enable HTTP content-security policy. This prevents scripts from executing in the browser. ## Protecting the DOM from XSS injection with React Modern JavaScript libraries/frameworks like React.js ensure data rendered by the DOM are sanitized by default. React.js is inspired by XHP. And XHP is a security fix written to minimize XSS attacks as much as possible. React provides a way to render markup in the DOM from data, even though it is not allowed out of the box. We use the `dangerouslySetInnerHTML` property on React elements. ## Additional Protection Against XSS Attacks If your application requires you to render markup from data in the DOM, below are some measures to take. - To properly secure a user’s cookie, use the HttpOnly response headers. - Avoid using browser storage to save sensitive user information. - Use HTTP security headers such as Content Security Policy (CSP). ## Secure Authentication Flow With Refresh tokens - Your authentication has two tokens: the access token and the refresh token. - The access token is used to get resources that require authentication from the API. It has a short duration. They are first created with login credentials along with the refresh tokens. As long as the refresh token has not yet expired, credentials are not needed to create access tokens. It is returned in the authentication response body. - Use the refresh token to create new access tokens. It has a longer duration. Refresh tokens are created when the user logs in with credentials. They are in the authentication response cookie. - Set the refresh token cookie with the httpOnly flag to prevent access via browser JavaScript. Use the secure=true flag so it can only be sent over HTTPS. - Save the access token in memory on the front end. Avoid using local storage. Refresh the access token for each page by making a call to the API to refresh. ## Why do we need BFF? Currently, most of the SPA applications are built in a way where tokens (access & refresh) are persisted in the browser. Typically tokens are stored in a session storage which exposes tokens to vulnerabilities and malicious code. *“Currently, SPAs have no means of keeping access and refresh tokens secure from malicious code. Even if developers attempt to protect their apps from XSS attacks (as they should), such an attack can still occur through a vulnerability in a third-party library. The only way to protect tokens from being accessed by any malicious code is to keep them away from the browser”*. [Duende](https://github.com/DuendeSoftware/BFF) recommends BFF implementation for all SPA applications. ## What is BFF in shortly? BFF is an intermediate layer (reverse proxy) between your SPA front end and API services. BFF enables the handling of the tokens and communication to the API services is handled in the backend. The BFF layer is protected with cookie-based authentication. This approach enables that tokens are not required to persist in the browser. **Traditional SPA architecture** ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/09/image-1.png?resize=640%2C150&ssl=1)**SPA architecture with BFF** ![undefined](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/09/image-2.png?resize=640%2C200&ssl=1)## Blazor project Now, we can create a new Blazor application hosted in an ASP.NET Core application. So, the solution has 3 projects: client, server and share. For the purpose of this post, the share project is useless. ### Change the server project In the server project. add the following NuGet packages: - Microsoft.AspNetCore.Authentication.OpenIdConnect - Duende.BFF Next, we will add OpenID Connect and OAuth support to the backend. For this, we are adding the Microsoft OpenID Connect authentication handler for the protocol interactions with the token service, and the cookie authentication handler for managing the resulting authentication session. The BFF services provide the logic to invoke the authentication plumbing from the front end (more about this later). Add the following snippet to your *Program.cs* above the call to *builder.Build();* ``` builder.Services.AddBff(); builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; }); ``` The last step is to add the required middleware for authentication, authorization and BFF session management. Add the following snippet after the call to *UseRouting*: ``` app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); app.MapBffManagementEndpoints(); ``` Finally, you can run the server project. This will start the host, which will in turn deploy the Blazor application to your browser. Try to manually invoke the BFF login endpoint on */bff/login* – this should bring you to the demo IdentityServer. After login (e.g. using bob/bob), the browser will return to the Blazor application. In other words, the fundamental authentication plumbing is already working. Now we need to make the front end aware of it. ### Change the client project A couple of steps are necessary to add the security and identity plumbing to a Blazor application. 1. Add the authentication/authorization related Nuget package called `Microsoft.AspNetCore.Components.WebAssembly.Authentication` and `Microsoft.Extensions.Http` 2. Add a using statement to *\_Imports.razor* to bring the above package in scope: ``` @using Microsoft.AspNetCore.Components.Authorization ``` 3. To propagate the current authentication state to all pages in your Blazor client, you add a special component called *CascadingAuthenticationState* to your application. This is done by wrapping the Blazor router with that component in *App.razor*: ``` Not found Sorry, there's nothing at this address. ``` 4. Last but not least, we will add some conditional rendering to the layout page to be able to trigger login/logout as well as displaying the current user name when logged in. This is achieved by using the *AuthorizeView* component in *MainLayout.razor*: ``` Hello, @context.User.Identity.Name! Log out Log in @Body ``` When you now run the Blazor application, you will see the following error in your browser console: ``` crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Cannot provide a value for property 'AuthenticationStateProvider' on type 'Microsoft.AspNetCore.Components.Authorization.CascadingAuthenticationState'. There is no registered service of type 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'. ``` *CascadingAuthenticationState* is an abstraction over an arbitrary authentication system. It internally relies on a service called *AuthenticationStateProvider* to return the required information about the current authentication state and the information about the currently logged on user. This component needs to be implemented, and that’s what we’ll do next. ### Modifying the frontend The BFF library has a server-side component that allows querying the current authentication session and state (see [here](https://docs.duendesoftware.com/identityserver/v6/bff/session/management/user/)). We will now add a Blazor *AuthenticationStateProvider* that will internally use this endpoint. Add a file with the following content: ``` using System.Net; using System.Net.Http.Json; using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; namespace Blazor6.Client.BFF; public class BffAuthenticationStateProvider : AuthenticationStateProvider { private static readonly TimeSpan UserCacheRefreshInterval = TimeSpan.FromSeconds(60); private readonly HttpClient _client; private readonly ILogger _logger; private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0); private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity()); public BffAuthenticationStateProvider( HttpClient client, ILogger logger) { _client = client; _logger = logger; } public override async Task GetAuthenticationStateAsync() { return new AuthenticationState(await GetUser()); } private async ValueTask GetUser(bool useCache = true) { var now = DateTimeOffset.Now; if (useCache && now < _userLastCheck + UserCacheRefreshInterval) { _logger.LogDebug("Taking user from cache"); return _cachedUser; } _logger.LogDebug("Fetching user"); _cachedUser = await FetchUser(); _userLastCheck = now; return _cachedUser; } record ClaimRecord(string Type, object Value); private async Task FetchUser() { try { _logger.LogInformation("Fetching user information."); var response = await _client.GetAsync("bff/user?slide=false"); if (response.StatusCode == HttpStatusCode.OK) { var claims = await response.Content.ReadFromJsonAsync(); var identity = new ClaimsIdentity( nameof(BffAuthenticationStateProvider), "name", "role"); foreach (var claim in claims) { identity.AddClaim(new Claim(claim.Type, claim.Value.ToString())); } return new ClaimsPrincipal(identity); } } catch (Exception ex) { _logger.LogWarning(ex, "Fetching user failed."); } return new ClaimsPrincipal(new ClaimsIdentity()); } } ``` and register it in the client’s *Program.cs*: ``` builder.Services.AddAuthorizationCore(); builder.Services.AddScoped(); ``` If you run the server app now again, you will see a different error: ``` fail: Duende.Bff.Endpoints.BffMiddleware[1] Anti-forgery validation failed. local path: '/bff/user' ``` This is due to the antiforgery protection that is applied automatically to the management endpoints in the BFF host. To properly secure the call, you need to add a static *X-CSRF* header to the call. See [here](https://docs.duendesoftware.com/identityserver/v6/bff/apis/local/) for more background information. This can be easily accomplished by a delegating handler that can be plugged into the default HTTP client used by the Blazor frontend. Let’s first add the handler: ``` public class AntiforgeryHandler : DelegatingHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { request.Headers.Add("X-CSRF", "1"); return base.SendAsync(request, cancellationToken); } } ``` and register it in the client’s *Program.cs* (overriding the standard HTTP client configuration; requires package Microsoft.Extensions.Http): ``` // HTTP client configuration builder.Services.AddTransient(); builder.Services.AddHttpClient("backend", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)) .AddHttpMessageHandler(); builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("backend")); ``` If you restart the application again, the logon/logoff logic should work now. In addition you can display the contents of the session on the main page by adding this code to *Index.razor*: ``` @page "/" Home Hello, Blazor BFF! @foreach (var claim in @context.User.Claims) { @claim.Type @claim.Value } ``` ### Securing the local API The standard Blazor template contains an API endpoint (*WeatherForecastController.cs*). Try invoking the weather page from the UI. It works both in logged in and anonymous state. We want to change the code to make sure, that only authenticated users can call the API. The standard way in ASP.NET Core would be to add an authorization requirement to the endpoint, either on the controller/action or via the endpoint routing, e.g.: ``` app.MapControllers() .RequireAuthorization(); ``` When you now try to invoke the API anonymously, you will see the following error in the browser console: ``` Access to fetch at 'https://demo.duendesoftware.com/connect/authorize?client_id=...[shortened]... (redirected from 'https://localhost:5002/WeatherForecast') from origin 'https://localhost:5002' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. ``` This happens because the ASP.NET Core authentication plumbing is triggering a redirect to the OpenID Connect provider for authentication. What we really want in that case is an API friendly status code – 401 in this scenario. This is one of the features of the BFF middleware, but you need to mark the endpoint as a BFF API endpoint for that to take effect: ``` app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); ``` After making this change, you should see a much better error message: `Response status code does not indicate success: 401 (Unauthorized).` The client code can properly respond to this, e.g. triggering a login redirect. When you logon now and call the API, you can put a breakpoint server-side and inspect that the API controller has access to the claims of the authenticated user via the *.User* property. **Categories:** .NET7, Blazor, Tips & tricks **Tags:** bff, blazor, blazor-server, blazor-webassembly, identity, identityserver4, xss **Hashtags:** blazor, xss --- ### [Azure DevOps pipeline for Maui](https://puresourcecode.com/dotnet/maui/azure-devops-pipeline-for-maui/) **Published:** August 18, 2023 **Author:** Enrico **Excerpt:** How do we create an Azure DevOps pipeline for building Maui components or applications? Here the base pipeline to use and customize **Content:** In this new post, I like to show you how I created an Azure DevOps pipeline for building Maui components. I binged around to see if there was something I could use. In the end, I started to play with the [YAML](https://puresourcecode.com/tools/what-is-yaml/). ## Scenario After my last post about a [component for Maiu](https://puresourcecode.com/dotnet/maui/custom-control-for-maui-using-skiasharp/), I wanted to create a pipeline using Azure DevOps and publish the component to [NuGet](https://www.nuget.org/). In order to publish your component to NuGet, you have to configure a Service Connection to the NuGet website via the settings in your project in Azure DevOps. For more details about the pipelines, I recommend reading my other posts: - [Ultimate pipeline for NuGet packages](https://puresourcecode.com/dotnet/net5/ultimate-pipeline-for-nuget-packages/) - [NuGet package versioning with DevOps](https://puresourcecode.com/tools/azure-devops/nuget-package-versioning-with-devops/) ## Implementation So, the component [PSC.Maui.Components.Doughnuts](https://puresourcecode.com/dotnet/maui/custom-control-for-maui-using-skiasharp/) works for all platforms and there is no customization for any platform. The component is built with NET7. So, it is pretty straightforward. ### Select your VM image In the new YAML file, we need to modify which VM image we’ll be using. Since we want one that contains [NET7](https://puresourcecode.com/category/dotnet/net7/), we’ll use `windows-2022`: ``` pool: vmImage: windows-2022 ``` As of writing, the `windows-latest` image has not yet been updated to use Visual Studio 2022 along with NET7, so we’ll have to explicitly set it to `windows-2022`. ### Install NET7 The next step is to install NET7 from the pipeline. Also, I have to specify what is the package type and where I want to install it. ``` - task: UseDotNet@2 displayName: 'Install .NET sdk' inputs: packageType: sdk version: 7.0.x installationPath: $(Agent.ToolsDirectory)/dotnet ``` ## Install the MAUI workload Next, we’ll install the NET MAUI workload onto the build agent. We need this to be able to build .NET MAUI apps. NET MAUI workload is an optional workload that you can install on top of the `NET SDK` to provide support for building cross-platform applications with NET Multi-platform App UI (NET MAUI). This can be done using the **Command Line** task and the `dotnet workload` command: ``` - task: CmdLine@2 inputs: script: 'dotnet workload install maui' ``` ### Restore packages In this component, I use SkiaSharp from a NuGet package and other packages. So, I have to restore all the packages in order to build successfully the package. Also, I use my feed in Azure DevOps. ``` - task: DotNetCoreCLI@2 displayName: Restore packages inputs: command: 'restore' feedsToUse: 'select' vstsFeed: 'your-nuget-feed' ``` ### Build the component and run tests Now, I’m going to build the project and run the tests. ``` - task: DotNetCoreCLI@2 displayName: Build project inputs: command: 'build' projects: '**/PSC.Maui.Components.Doughnuts.csproj' arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: Run tests inputs: command: 'test' projects: '**/*[Te]ests/*.csproj' arguments: '--configuration $(buildConfiguration) --no-build' ``` ### Prepare the package Now, the build is ready. I want to prepare the NuGet package to push on NuGet. **Be careful**: there are some variables here like `PackageVersion` that you have to add to your `Variables` pipeline. ``` - task: DotNetCoreCLI@2 displayName: Prepare the package inputs: command: 'pack' packagesToPack: '**/PSC.Maui.Components.Doughnuts.csproj' versioningScheme: 'byEnvVar' versionEnvVar: 'PackageVersion' arguments: '-t:pack' ``` ### Publish the package in the artifacts Now, the next step is to publish the NuGet package in the artifacts in Azure DevOps. ``` - task: DotNetCoreCLI@2 displayName: Publish the package inputs: command: 'push' packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg' nuGetFeedType: 'internal' publishVstsFeed: 'your-api-code' ``` ### Publish to NuGet.org Finally, the last task is to push the NuGet package on [Nuget.org](https://www.nuget.org/) ``` - task: NuGetCommand@2 displayName: Push the package to NuGet.org inputs: command: 'push' packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' nuGetFeedType: 'external' publishFeedCredentials: 'NuGet Website' ``` ### Variables In this pipeline, I use variables to track the version of the package. Then, add the following variables: - **Major**: the major version of your package. In the image below is 1 - **Minor**: the minor version of your package. In the image below is 0 - **PackageVersionType**: this is the place where you can specify if this version is `-pre` release, `-alfa`, `-beta` or whatever you prefer - **Patch**: here is where the magic happens. I’ll explain in a second - **PackageVersion**: creating the string for the version of this package With this, I have an incremental, automatic Patch-version of my **PackageVersion** variable, with **Major** and **Minor** being updated manually by yours truly. I also have the optional **PackageVersionType**, in case I want to label a package explicitly as being a “preview” or anything else. For more details and a walkthrough of those variables, read my post “[NuGet package versioning with DevOps](https://puresourcecode.com/tools/azure-devops/nuget-package-versioning-with-devops/)“. ## The full YAML Here is the entire YAML file for future reference. ``` trigger: - main pool: vmImage: windows-2022 steps: - task: UseDotNet@2 displayName: 'Install .NET sdk' inputs: packageType: sdk version: 7.0.x installationPath: $(Agent.ToolsDirectory)/dotnet - task: CmdLine@2 displayName: 'Install Maui Workload' inputs: script: 'dotnet workload install maui' - task: DotNetCoreCLI@2 displayName: Restore packages inputs: command: 'restore' feedsToUse: 'select' vstsFeed: 'your-nuget-feed' - task: DotNetCoreCLI@2 displayName: Build project inputs: command: 'build' projects: '**/PSC.Maui.Components.Doughnuts.csproj' arguments: '--configuration $(buildConfiguration)' - task: DotNetCoreCLI@2 displayName: Run tests inputs: command: 'test' projects: '**/*[Te]ests/*.csproj' arguments: '--configuration $(buildConfiguration) --no-build' - task: DotNetCoreCLI@2 displayName: Prepare the package inputs: command: 'pack' packagesToPack: '**/PSC.Maui.Components.Doughnuts.csproj' versioningScheme: 'byEnvVar' versionEnvVar: 'PackageVersion' arguments: '-t:pack' - task: DotNetCoreCLI@2 displayName: Publish the package inputs: command: 'push' packagesToPush: '$(Build.ArtifactStagingDirectory)/*.nupkg' nuGetFeedType: 'internal' publishVstsFeed: 'your-api-code' - task: NuGetCommand@2 displayName: Push the package to NuGet.org inputs: command: 'push' packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' nuGetFeedType: 'external' publishFeedCredentials: 'NuGet Website' ``` ## Wrap up In conclusion, this is how to build, test and publish in the Azure DevOps pipeline for Maui component. Please feel free to use the [forum](https://puresourcecode.com/forum/) to ask any questions. **Categories:** Azure DevOps, MAUI **Tags:** azure-devops, maui **Hashtags:** azure-devops, maui --- ### [Custom control for MAUI using SkiaSharp](https://puresourcecode.com/dotnet/csharp/custom-control-for-maui-using-skiasharp/) **Published:** August 17, 2023 **Author:** Enrico **Excerpt:** I will demonstrate how you can create your own custom control for MAUI using SkiaSharp and what you need to do in order to make it reusable **Content:** In this blog post, I will demonstrate how you can create your own custom control for MAUI using [SkiaSharp](https://github.com/mono/SkiaSharp) and what you need to do in order to make it reusable. The full source code of this component is on [GitHub](https://github.com/erossini/PSC.Maui.Components.Doughnuts). Also, you can use the NuGet package [PSC.Maui.Components.Doughnuts](https://www.nuget.org/packages/PSC.Maui.Components.Doughnuts/). ## What is SkiaSharp? SkiaSharp is a cross-platform 2D graphics API for .NET platforms based on Google’s Skia Graphics Library ([skia.org](https://skia.org/)). It provides a comprehensive 2D API that can be used across mobile, server and desktop models to render images. SkiaSharp provides cross-platform bindings for: - .NET Standard 1.3 - .NET Core - .NET 6 - Tizen - Android - iOS - tvOS - macOS - Mac Catalyst - WinUI 3 (Windows App SDK / Uno Platform) - Windows Classic Desktop (Windows.Forms / WPF) - Web Assembly (WASM) - Uno Platform (iOS / macOS / Android / WebAssembly) The [API Documentation](https://docs.microsoft.com/en-us/dotnet/api/SkiaSharp/) is available on the web to browse. ## The Angle Arc The angle arc approach to drawing arcs requires that you specify a rectangle that bounds an ellipse. The arc on the circumference of this ellipse is indicated by angles from the center of the ellipse that indicate the beginning of the arc and its length. Two different methods draw angle arcs. These are the [`AddArc`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skpath.addarc#skiasharp-skpath-addarc(skiasharp-skrect-system-single-system-single)) method and the [`ArcTo`](https://learn.microsoft.com/en-us/dotnet/api/skiasharp.skpath.arcto#skiasharp-skpath-arcto(skiasharp-skrect-system-single-system-single-system-boolean)) method: ``` public void AddArc (SKRect oval, Single startAngle, Single sweepAngle) public void ArcTo (SKRect oval, Single startAngle, Single sweepAngle, Boolean forceMoveTo) ``` These methods are identical to the Android [`AddArc`](https://learn.microsoft.com/en-us/dotnet/api/android.graphics.path.addarc) and `[ArcTo]xref:Android.Graphics.Path.ArcTo*)` methods. The iOS [`AddArc`](https://learn.microsoft.com/en-us/dotnet/api/coregraphics.cgpath.addarc#coregraphics-cgpath-addarc(system-nfloat-system-nfloat-system-nfloat-system-nfloat-system-nfloat-system-boolean)) method is similar but is restricted to arcs on the circumference of a circle rather than generalized to an ellipse. Both methods begin with an `SKRect` value that defines both the location and size of an ellipse: ![The oval that begins an angle arc](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/anglearcoval.png?w=640&ssl=1) The arc is a part of the circumference of this ellipse. ### startAngle and sweepAngle The `startAngle` argument is a clockwise angle in degrees relative to a horizontal line drawn from the center of the ellipse to the right. The `sweepAngle` argument is relative to the `startAngle`. Here are `startAngle` and `sweepAngle` values of 60 degrees and 100 degrees, respectively: ![The angles that define an angle arc](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/anglearcangles.png?w=640&ssl=1) The arc begins at the start angle. Its length is governed by the sweep angle. The arc is shown here in red: ![The highlighted angle arc](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/anglearchighlight.png?w=640&ssl=1) The curve added to the path with the `AddArc` or `ArcTo` method is simply that part of the ellipse’s circumference: ![The angle arc by itself](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/anglearc.png?w=640&ssl=1) The `startAngle` or `sweepAngle` arguments can be negative: The arc is clockwise for positive values of `sweepAngle` and counter-clockwise for negative values. However, `AddArc` does *not* define a closed contour. If you call `LineTo` after `AddArc`, a line is drawn from the end of the arc to the point in the `LineTo` method, and the same is true of `ArcTo`. `AddArc` automatically starts a new contour and is functionally equivalent to a call to `ArcTo` with a final argument of `true`: ``` path.ArcTo (oval, startAngle, sweepAngle, true); ``` That last argument is called `forceMoveTo`, and it effectively causes a `MoveTo` call at the beginning of the arc. That begins a new contour. That is not the case with a last argument of `false`: ``` path.ArcTo (oval, startAngle, sweepAngle, false); ``` This version of `ArcTo` draws a line from the current position to the beginning of the arc. This means that the arc can be somewhere in the middle of a larger contour. ## The goal Now, the idea is to create a component to create a wheel or doughnut (like in iOS) that will look like this: ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-9.png?w=200&ssl=1) All the colours of the Wheel must be customizable. Just to understand a bit more about SkiaSharp and how it works. ## Wheel/Doughnut properties Now, for the doughnut, we want to have some custom properties to customize the colours. Here are the properties: - **InnerColor**: this is the colour of the background of the Wheel - **SweepAngle**: the angle to be selected/highlighted starting from 0 - **WheelColor**: the base colour of the Wheel/Doughnut - **WheelSelectedColor**: the colour of the selected/highlighted ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-10.png?resize=640%2C244&ssl=1)### Setup Now, in order to start this simple project, we need a new *.NET MAUI Class Library* project. This is where we will actually implement our custom control. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-4.png?resize=640%2C426&ssl=1)To create a component for Maui, add a new project and select `.NET MAUI Class Library`. After that give a name (in my case is PSC.Maui.Components.Doughnuts). ### Clean up Now, the basic project is created and we have a boilerplate for a new component. The implementation will be one for all platforms. So, open the folder `Platforms` and in each folder delete the file `PlatformClass1`. ## Add SkiaSharp Next, we need to add **SkiaSharp** to our class library project. For this, we add the following packages in the NuGet package manager: - SkiaSharp.Views.Maui.Controls *(version 2.88.3 at the time of writing)* - SkiaSharp.Views.Maui.Core *(version 2.88.3 at the time of writing)* ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-5.png?resize=640%2C279&ssl=1)Once installed, we can use the **SKCanvasView** as a base class for our control. After that, our class should look like this: ``` using SkiaSharp; using SkiaSharp.Views.Maui; using SkiaSharp.Views.Maui.Controls; namespace PSC.Maui.Components.Doughnuts { // All the code in this file is included in all platforms. public class Doughnut : SKCanvasView { } } ``` ### Handler Registration What is a handler? All handler-based .NET MAUI controls support two handler lifecycle events: - `HandlerChanging` is raised when a new handler is about to be created for a cross-platform control, and when an existing handler is about to be removed from a cross-platform control. The `HandlerChangingEventArgs` object that accompanies this event has `NewHandler` and `OldHandler` properties, of type `IElementHandler`. When the `NewHandler` property isn’t `null`, the event indicates that a new handler is about to be created for a cross-platform control. When the `OldHandler` property isn’t `null`, the event indicates that the existing native control is about be removed from the cross-platform control, and therefore any native events should be unwired and other cleanup performed. - `HandlerChanged` is raised after the handler for a cross-platform control has been created. This event indicates that the native control that implements the cross-platform control is available, and all the property values set on the cross-platform control have been applied to the native control. To know more about the handlers, see the official [Microsoft documentation](https://learn.microsoft.com/en-us/dotnet/maui/user-interface/handlers/create). After what I said, we need to register a handler for our control. This is required because otherwise, MAUI doesn’t know how to render the control for each platform. Because we don’t need any platform-specific handlers since we inherit directly from `SKCanvasView`, we can use the existing `SKCanvasViewHandler` from *SkiaSharp*. In order to register the handler for our control, we need to create a static class inside our **PSC.MAUI.Components.Doughnuts** project that I usually call `Registration`. In this class, we create an extension method called `UseDoughnut()` where we add the handler to the `MauiAppBuilder`: ``` public static class Registration { public static MauiAppBuilder UseDoughnut(this MauiAppBuilder builder) { builder.ConfigureMauiHandlers(h => { h.AddHandler(); }); return builder; } } ``` This can now be used in the main project’s `MauiProgram` class as follows: ``` using PSC.Maui.Components.Doughnuts; public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() .UseDoughnut() //add this line .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); return builder.Build(); } } ``` ### Adding the control to XAML Although we haven’t implemented yet the component, I can already add the `Doughnut` to a XAML *Page* or *View*. So, we have to import the namespace from our class library and add the control to the layout (in the following code, the name is `dn`): ``` ``` Probably, we receive an error with this code because the properties are not defined yet. ## Implementing the Wheel/Doughnut Now, we can start with the component itself. The first thing to do is define the `BindableProperty`. ### What is a `BindableProperty`? A `BindableProperty` is a special type of property that can be used in .NET MAUI apps to support data binding, styles, templates, and other features. It is defined by a class that inherits from `BindableObject`, and it can be accessed by other classes as an attached property. You can also use a source generator to automatically create `BindableProperties` from fields. ### Implementing a property The first thing we want to be sure is: if we change a property, the wheel has to be redraw. For this purpose, we can invalidate the `canvas` on where we draw our wheel. This component is inherited from the `SKCanvasView`. So, we have a command to invalidate everything and start to draw again. This function is `InvalidateSurface` and I call it in a generic function `OnAnyPropertyChanged` ``` private static void OnAnyPropertyChanged(BindableObject bindable, object oldValue, object newValue) { ((Doughnut)bindable).InvalidateSurface(); } ``` Every time a property changes its value, I want to call this function. Now,let me create the `InnerColor` property. ``` public Color InnerColor { get => (Color)GetValue(InnerColorProperty); set => SetValue(InnerColorProperty, value); } ``` This is a quite normal property with `get` and `set`. If you notice, it calles something called `InnerColorProperty`. This is the real bindable property that we can call from the UI. Here the implementation: ``` public static readonly BindableProperty InnerColorProperty = BindableProperty.Create( nameof(InnerColor), typeof(Color), typeof(Doughnut), Color.FromArgb("#ffffffff"), propertyChanged: OnAnyPropertyChanged); ``` I’m doing the some implementation for the other properties. ### Draw the circle Now, the most exciting part. Skia gives us the **canvas** where we can draw. Based on the abode documentation related to the `Arc`, I can use this info to draw in the center of the canvas a circle or part of it and create the illusion that the Wheel or Doughnut – as use like to call it – it is empty. ``` private void DrawCircle(SKImageInfo info, SKCanvas canvas, Color color, float Radius, float startAngle, float sweepAngle) { var center = new SKPoint(info.Width / 2F, info.Height / 2F); using (var path = new SKPath()) using (var fillPaint = new SKPaint()) { fillPaint.Style = SKPaintStyle.Fill; fillPaint.Color = color.ToSKColor(); var radius = Math.Min(info.Width / 2, info.Height / 2) * Radius; var rect = new SKRect(center.X - radius, center.Y - radius, center.X + radius, center.Y + radius); path.MoveTo(center); path.ArcTo(rect, startAngle, Math.Abs(sweepAngle - 360F) < EPSILON ? 359.99F : sweepAngle, false); path.Close(); canvas.DrawPath(path, fillPaint); } } ``` ### Draw all circles and the magic begins The function above draws only one circle in the middle of the canvas. Let see the code first. ``` protected override void OnPaintSurface(SKPaintSurfaceEventArgs e) { base.OnPaintSurface(e); canvas = e.Surface.Canvas; canvas.Clear(); // clears the canvas for every frame info = e.Info; drawRect = new SKRect(0, 0, info.Width, info.Height); // where the pie starts var startAngle = -90F; var center = new SKPoint(info.Width / 2F, info.Height / 2F); float bigWheelRadius = 0.96F; float bigAngle = 360F; float interWheelRadius = 0.96F; float miniWheelRadius = 0.84F; // draw the big doughnut DrawCircle(info, canvas, WheelSelectedColor, 1, startAngle, bigAngle); if (ShowNoData) { DrawCircle(info, canvas, WheelColor, 1, -93, 6); DrawCircle(info, canvas, InnerColor, miniWheelRadius, startAngle, bigAngle); } else { DrawCircle(info, canvas, WheelColor, 1, startAngle, SweepAngle); DrawCircle(info, canvas, InnerColor, miniWheelRadius, startAngle, bigAngle); } } ``` Here I have the `OnPaintSurface` that is called when the component starts of a property changed. This override tha function from `SkiaSharp`. Now, from the `SKPaintSurfaceEventArgs e` I have to read the `Canvas` where I can draw the wheel. For this reason, I use ``` canvas = e.Surface.Canvas; ``` Now, I have the canvas. I want to clean it with ``` canvas.Clear(); ``` Next step is to know how big is the canvas using the `info = e.Info;`. Then, I start to draw the circle to create the idea of wheel\\doughnut. ## Wrap up In conclusion, this is how to create a custom control for MAUI using SkiaSharp the you can re-use in your applications. **Categories:** .NET7, C#, MAUI **Tags:** maui, skia **Hashtags:** maui --- ### [Switch to dark mode automatically on Windows](https://puresourcecode.com/tools/switch-to-dark-mode-automatically-on-windows/) **Published:** August 11, 2023 **Author:** Enrico **Content:** In this post, I talk about to switch to dark mode automatically on Windows. Why? Although Windows 11 comes with an option to switch between the light and dark modes, it’s a manual process that requires several steps. Auto Dark Mode X is an open-source application that allows you to configure a schedule to switch between the light and dark modes (and vice versa) automatically. Furthermore, the application also includes exceptions to prevent the switch while you are gaming or running on battery. You can even specify whether apps, including Microsoft Office, should follow the colour or stay in their current theme. Although [Windows 11](https://puresourcecode.com/tag/windows11/) comes with an option to switch between the light and dark modes, it’s a manual process that requires several steps. **Auto Dark Mode X** is an open-source application that allows you to configure a schedule to switch between the light and dark modes (and vise versa) automatically. Furthermore, the application also includes exceptions to prevent the switch while you are gaming or running on battery. You can even specify whether apps, including Microsoft Office, should follow the colour or stay in their current theme. In this guide, you will learn the steps to use a third-party tool to add the ability to configure a schedule to switch between the light and dark modes automatically. (Auto Dark Mode X also works on [Windows 10](https://puresourcecode.com/tag/windows10/).) ## Change light to dark mode automatically on Windows 11 Before you can switch between colour modes automatically, you need to install the application, which is available through GitHub, but you can also install it with the Windows Package Manager tool built into Windows. ### Install Auto Dark Mode X To install Auto Dark Mode X, use these steps: 1. Open **Start**. 2. Search for **Command Prompt**, right-click the top result, and select the **Run as administrator** option. 3. Type the following command to download and install the Auto Dark Mode X app and press **Enter** ``` winget install --id Armin2208.WindowsAutoNightMode ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image.png?resize=640%2C292&ssl=1)Command prompt Once you complete the steps, you can launch the application and configure the schedule to switch between the light and dark modes automatically. You can also download the installer from the [**official GitHub page**](https://github.com/AutoDarkMode/Windows-Auto-Night-Mode/releases) to install the application manually. ### Schedule light to dark mode automatic switch To switch between the light and dark colour modes automatically on a schedule, use these steps: 1. Open **Start** on Windows 11. 2. Search for **Auto Dark Mode X** and click the top result to open the app. 3. Click on **Time**. 4. Select the **Set custom hours** option. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-1.png?resize=640%2C463&ssl=1)Auto Dark Mode Custom Time 5. Specify the time when Windows 11 should switch to the light mode. 6. Specify the time when to switch to the dark mode. 7. (Optional) Select the **From sunset to sunrise** option to automatically switch between the two system colour modes during sunset and sunrise. 8. Click on **Switch Modes**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-2.png?resize=640%2C465&ssl=1)Auto Dark Mode Switch Modes 9. (Optional) Check the Don’t switch while playing video games option. 10. (Optional) Check the Battery powered devices option. 11. Click on Apps. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/08/image-3.png?resize=640%2C465&ssl=1)Auto Dark Mode Apps 12. Use the **Apps** option to decide whether compatible apps should follow the system colour mod or stay on the light or dark mode. 13. Use the **System** option to decide whether Windows 11 should follow the system color mod or stay on the light or dark mode. 14. Use the **Microsoft Office** option to decide whether Word, Excel, PowerPoint, and Outlook should follow the system color mod or stay on the light or dark mode. After you complete the steps, Windows 11 will switch between light and dark mode (or vice versa) depending on your configuration. **Categories:** Tools, Windows --- ### [Testing PowerShell with Pester for real](https://puresourcecode.com/tools/powershell/testing-powershell-with-pester-for-real/) **Published:** May 9, 2023 **Author:** Enrico **Excerpt:** Do you want to test your script to delete files in a production environment? Here an explanation for testing PowerShell with Pester for real **Content:** Following my previous post about [testing PowerShell](https://puresourcecode.com/tools/powershell/testing-powershell-scripts-with-pester/) with Pester, in this post I show you my way of testing PowerShell with [Pester](https://pester.dev/) in the real world. ## Scenario Consider the following scenario. You want to smoke-test your application using a bunch of files designed for that. Also, you have different environments, such as the development DEV or UAT environments. Every environment has a name for the environment, like `dev` for the development environment, and the number of the environment (for example dev1, uat2…). For each environment, I have a folder with test files to remove. In the following script, I search in the environment folders and subfolders and look at the name of the file that contains the words `tmp` and `test` to delete. The `CleanupFilesFiter.ps1` is checking if the parameters are valid and verifying if the environment has the expected format; read the folders and subfolders to detect what files it has to delete. Because the script has to delete for real the files in a production environment, I want to be sure it works as expected. For this reason, I want to test it and the easy way is to use Pestel to mock the files or create the structure I need to test the script. ## CleanupFilesFilter.ps1 So, I break down the script to better understand what I’m doing here. First, the parameters require to run the script. ### The parameters Now, the parameters the script is expected are: - the `targetPath` when it has to search for the files to delete - the `targetEnv` (target environment) to determine what environment folder it has to check - the file creation date `RemoveAfterDate` is used to filter the files to remove after this date ``` param( [Parameter(Mandatory=$true)] [string]$targetPath, [Parameter(Mandatory=$true)] [string]$targetEnv, [Parameter(Mandatory=$true)] [ValidateScript({[DateTime]::ParseExact($_, "yyyy-MM-dd HH:mm", $null)})] [string]$RemoveAfterDate ) ``` ### The folder list In the environment folder, I can have lot of folders. So, I want to limit the search in specific folders listed in this variable. ``` [string[]]$folderList = "Folder1","Folder2","Folder3","Folder4","Folder5" ``` ### Validate the environment As I said before, the files are in an environment folder. This folder has the conventional short name plus the number of the environment. For example, a valid environment is `dev1` or `UAT3`. To check if the environment string is valid, I use the Regex for it. ``` function ValidateEnv([string]$theEnv) { if ($theEnv -eq "") { Write-Error "Environment is an empty string" return $false } $matchText = "^(dev\d?$|prd\d?$)" if ($theEnv -notmatch $matchText) { Write-Error "$($theEnv) is not a valid environment, please use dev optionally followed by a digit" return $false } return $true } ``` ### Check files to delete Now, here is where the magic happens. This function checks in a specific folder if there is any file that matches the name or the date. Notice that I define and return a `fileList` variable that is a `[System.Collections.Generic.List[System.IO.FileInfo]]`. This will be useful when I test the function. If there are some files, the script adds the list of files to the `fileList`. The `list` of files from `Get-ChildItem` is converted into an array of `System.IO.FileInfo`. ``` function CheckFilesToDelete([string]$EnvPath, [string[]]$fList, [datetime]$date) { $fileList = [System.Collections.Generic.List[System.IO.FileInfo]]::new() foreach ($folderName in $fList) { $path = "$EnvPath\$folderName" $list = @() if($date -eq $null) { $list = [System.IO.FileInfo[]]@(Get-ChildItem -Path $path -File -Recurse | Where-Object { $_.Name -match '.*Tmp ?Test.*' }) } else { $list = [System.IO.FileInfo[]]@(Get-ChildItem "$path" -File -Recurse | Where-Object { $_.CreationTime -gt $date }) } if ($list.Count -gt 0) { $fileList.AddRange($list) } } return $fileList } ``` ### File deletion Finally, the last function of the script is for deleting the files using the function `CheckFilesToDelete` described above. This function checks for every folder in the environment folder the files to delete and add them in the `FilesToDelete` variable. In the following script, the delete command in commented and you will see the list of files to delete. If you want to delete for real, just uncomment that line. ``` function GetData() { foreach($folderName in $folderList) { $path = "$directoryEnvPath\$folderName" $checkPath = Test-Path $path if ($checkPath -eq $false) { continue; } if($filterByDate -eq $true) { ($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null } else { ($FilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null } } if($filterByDate -eq $true) { ($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName -date $RemoveAfterDate)) > $null } if($FilesToDelete.Count) { $FilesToDelete #$FilesToDelete | Remove-Item -Verbose -Recurse } else { Write-Host "`n No smoke test files found to delete in $($directoryEnvPath) `n" -ForegroundColor Yellow } if($NewerFilesToDelete.Count) { $NewerFilesToDelete #$NewerFilesToDelete | Remove-Item -Verbose -Recurse } else { Write-Host "`n No newer files then ${RemoveAfterDate} found to delete in ${directoryEnvPath} `n" -ForegroundColor Yellow } } ``` ### The full script Now, for your convenient, I post here the full script called `CleanupFilesFilter.ps1`. ``` param( [Parameter(Mandatory=$true)] [string]$targetPath, [Parameter(Mandatory=$true)] [string]$targetEnv, [Parameter(Mandatory=$true)] [ValidateScript({[DateTime]::ParseExact($_, "yyyy-MM-dd HH:mm", $null)})] [string]$RemoveAfterDate ) $ErrorActionPreference = "Stop" $filterByDate = $false [string[]]$folderList = "Folder1","Folder2","Folder3","Folder4","Folder5" function ValidateEnv([string]$theEnv) { if ($theEnv -eq "") { Write-Error "Environment is an empty string" return $false } $matchText = "^(dev\d?$|prd\d?$)" if ($theEnv -notmatch $matchText) { Write-Error "$($theEnv) is not a valid environment, please use dev optionally followed by a digit" return $false } return $true } function CheckFilesToDelete([string]$EnvPath, [string[]]$fList, [datetime]$date) { $fileList = [System.Collections.Generic.List[System.IO.FileInfo]]::new() foreach ($folderName in $fList) { $path = "$EnvPath\$folderName" $list = @() if($date -eq $null) { $list = [System.IO.FileInfo[]]@(Get-ChildItem -Path $path -File -Recurse | Where-Object { $_.Name -match '.*Tmp ?Test.*' }) } else { $list = [System.IO.FileInfo[]]@(Get-ChildItem "$path" -File -Recurse | Where-Object { $_.CreationTime -gt $date }) } if ($list.Count -gt 0) { $fileList.AddRange($list) } } return $fileList } function GetData() { foreach($folderName in $folderList) { $path = "$directoryEnvPath\$folderName" $checkPath = Test-Path $path if ($checkPath -eq $false) { continue; } if($filterByDate -eq $true) { ($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null } else { ($FilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName)) > $null } } if($filterByDate -eq $true) { ($NewerFilesToDelete += @(CheckFilesToDelete -EnvPath $directoryEnvPath -fList $folderName -date $RemoveAfterDate)) > $null } if($FilesToDelete.Count) { $FilesToDelete #$FilesToDelete | Remove-Item -Verbose -Recurse } else { Write-Host "`n No smoke test files found to delete in $($directoryEnvPath) `n" -ForegroundColor Yellow } if($NewerFilesToDelete.Count) { $NewerFilesToDelete #$NewerFilesToDelete | Remove-Item -Verbose -Recurse } else { Write-Host "`n No newer files then ${RemoveAfterDate} found to delete in ${directoryEnvPath} `n" -ForegroundColor Yellow } } # # Start Write-Host "This script will delete files and folders created by testing" ValidateEnv $targetEnv > $null $targetPathEnv = "$targetPath\$targetEnv" $checkPath = Test-Path $targetPathEnv if ($checkPath -eq $false) { Write-Error "$($targetPathEnv) doesn't exist" } if($RemoveAfterDate -ne "") { $filterByDate = $true } $directoryEnvPath = "$targetPathEnv\TestFolder" [System.IO.DirectoryInfo[]]$FilesToDelete GetData ``` ## CleanupFilesFilter.Tests.ps1 Now, I explained in the [previous post](https://puresourcecode.com/tools/powershell/testing-powershell-scripts-with-pester/), it is common for a PowerShell script test to have a file with the same name of the file to test plus `.Test` before the extension. ### BeforeAll So, this section is called from Pester before anything else. Therefore, here is the right place to add the code to create folders and files for testing the script. ``` BeforeAll { $path = Join-Path -Path $targetPath -ChildPath $targetEnv Write-Host "This is the path $path" Write-Host "" Write-Host "Creating folders and files..." $path = Join-Path -Path $path -ChildPath $mainFolder $folderList | ForEach-Object { $tmpPath = Join-Path $path $_ New-Item $tmpPath -ItemType Directory -force ("TmpTest","Tmp Test","tmp-test","Tmp 1Test","test") | foreach { New-Item -Path $tmpPath -Name "$_.txt" -Force } } Write-Host "Creation completed" . $PSCommandPath.Replace('.Tests.ps1', '.ps1') -targetPath $targetPath -targetEnv $targetEnv -RemoveAfterDate $RemoveAfterDate } ``` As you can see, with `New-Item` I force the creation of the directory and then with the same command force the creation of same files in the folder. When the creation process is completed, I run the script I want to test. ### Validate the environment First, I want to test the function `ValidateEnv` to check if it returns what I expect with different input. Remember, the environment has to have the environment code plus a number. ``` Describe "Validate Environment" { Context "when environment is empty" { It "should return 'Environment is an empty string'" { $scriptBlock = { ValidateEnv -theEnv '' -ErrorAction Stop } $scriptBlock | Should -Throw "Environment is an empty string" } } Context "when environment has more than 1 digit" { It "should return 'Environment is an empty string'" { $tmpEnv = 'dev01' $scriptBlock = { ValidateEnv -theEnv $tmpEnv -ErrorAction Stop } $scriptBlock | Should -Throw "$($tmpEnv) is not a valid environment, please use dev optionally followed by a digit" } } ``` In case the `ValidateEnv` finds the name of the environment is wrong, it raise an error with `Write-Error... return $false`. This causes an issue also in the test script. For this reason, I define the variable `scriptBlock` and trap the error and compare the returned string. ### Check files Then, this is the most complicated part of the all scripts. Because I want to verify if the function picks up the right files, I have to create an object in PowerShell and Pester for a file with all the details. #### Mock Get-ChildItem Now, let me start with the simple one. Because I want to test different thing, I need different files. Pester provides a `Mock` for a lot of commands. In particular, I have to mock the result of `Get-ChildItem` that contains in this case file details. ``` Mock Get-ChildItem { $arr = @( [System.IO.FileInfo]::new ('Tmp 1Test.txt'), [System.IO.FileInfo]::new ('tmp-test.txt'), [System.IO.FileInfo]::new ('TmpTest.txt'), [System.IO.FileInfo]::new ('qq02000.doc')) ``` In this code, the `Mock` for `Get-ChildItem` has an array `arr` with 4 files only with their names. So, I apply the filter on this `Mock` with ``` $arr | Where-Object { $_.Name -match '.*Tmp ?Test.*' } ``` that extracts from the list only the files with `Tmp Test` in the file name. Then, I define a variable to list the files I expected to have from the function I want to test. ``` $expected = [System.Collections.Generic.List[System.IO.FileInfo]]::new() $expected.Add([System.IO.FileInfo]::new('TmpTest.txt')) ``` Now, I’m calling the function` CheckFilesToDelete` and it returns a `list` of files. Then, I sort the list of files and take only the name of the files not all the attributes and check the list with the `expected` list. ``` $list = CheckFilesToDelete -EnvPath $path -fList $folderList -ErrorAction Stop $list.Name | Sort-Object | Should -Be ($expected.Name | Sort-Object Name) ``` If the `list` has the same files of `expected`, the test is passed. And it passed. ### Complex Mock Get-ChildItem So far so good. My initial idea was to test the creation date of files to decide if delete them or not. In PowerShell I couldn’t find a way to create a file and change the creation date and time. For this reason, I have to create same `MockObject` to add in the `Mock Get-ChildItem`. ``` Mock Get-ChildItem { $arr = @( (New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'Tmp Test.txt'; CreationTime = [datetime]'2023-01-01 21:00:00' }), (New-MockObject - Type 'System.IO.FileInfo' - Properties @{ Name = 'tmp-test.txt'; CreationTime = [datetime]'2023-01-01 22:00:00' }), (New-MockObject - Type 'System.IO.FileInfo' - Properties @{ Name = 'TmpTest.txt'; CreationTime = [datetime]'2022-01-01 22:00:00' }), (New-MockObject - Type 'System.IO.FileInfo' - Properties @{ Name = 'qq02000.doc'; CreationTime = [datetime]'2020-01-01 22:15:00' }) ) ``` So, using here I use the Pester command `New-MockObject` to create a list of files and add the `CreationTime` I want. So, I can test also the part of the function when I want to test the date of the files. ### The full test script ``` # Define variables $targetPath = $PSScriptRoot $targetEnv = 'dev1' $mainFolder = "TestFolder" $RemoveAfterDate = "2022-01-01 00:00" [string[]]$folderList = "Folder1","Folder2","Folder3","Folder4","Folder5" # Setup the tests environment BeforeAll { $path = Join-Path -Path $targetPath -ChildPath $targetEnv Write-Host "This is the path $path" Write-Host "" Write-Host "Creating folders and files..." $path = Join-Path -Path $path -ChildPath $mainFolder $folderList | ForEach-Object { $tmpPath = Join-Path $path $_ New-Item $tmpPath -ItemType Directory -force ("TmpTest","Tmp Test","tmp-test","Tmp 1Test","test") | foreach { New-Item -Path $tmpPath -Name "$_.txt" -Force } } Write-Host "Creation completed" . $PSCommandPath.Replace('.Tests.ps1', '.ps1') -targetPath $targetPath -targetEnv $targetEnv -RemoveAfterDate $RemoveAfterDate } # Validate environment function Describe "Validate Environment" { Context "when environment is empty" { It "should return 'Environment is an empty string'" { $scriptBlock = { ValidateEnv -theEnv '' -ErrorAction Stop } $scriptBlock | Should -Throw "Environment is an empty string" } } Context "when environment has more than 1 digit" { It "should return 'Environment is an empty string'" { $tmpEnv = 'dev01' $scriptBlock = { ValidateEnv -theEnv $tmpEnv -ErrorAction Stop } $scriptBlock | Should -Throw "$($tmpEnv) is not a valid environment, please use dev optionally followed by a digit" } } Context "when environment is valid" { It "should return true" { ValidateEnv -theEnv $targetEnv | Should -Be $true } } } Describe "Validate files to delete" { Context "validate files with name" { It "should return a list of expected files (mock)" { [string[]]$folderList = "Tests" $expected = [System.Collections.Generic.List[System.IO.FileInfo]]::new() $expected.Add([System.IO.FileInfo]::new('TmpTest.txt')) Mock Get-ChildItem { $arr = @( [System.IO.FileInfo]::new('Tmp 1Test.txt'), [System.IO.FileInfo]::new('tmp-test.txt'), [System.IO.FileInfo]::new('TmpTest.txt'), [System.IO.FileInfo]::new('qq02000.doc')) # Mocking the -Filter parameter $arr | Where-Object { $_.Name -match '.*Tmp ?Test.*' } } $list = CheckFilesToDelete -EnvPath $path -fList $folderList -ErrorAction Stop $list.Name | Sort-Object | Should -Be ($expected.Name | Sort-Object Name) } It "should return a list of expected files (file system)" { [System.Collections.Generic.List[System.Object]]$expected = @( [PSCustomObject]@{ Name = 'Tmp Test.txt' }, [PSCustomObject]@{ Name = 'tmp-test.txt' } ) $list = CheckFilesToDelete -EnvPath $path -fList $folderList -ErrorAction Stop $list.Count | Should -Be 10 $list[0].Name | Should -Be 'Tmp Test.txt' } } Context "validate files with date" { It "should return a list of expected files (mock)" { [string[]]$folderList = "Tests" $expected = [System.Collections.Generic.List[System.IO.FileInfo]]::new() $expected.Add([System.IO.FileInfo]::new('TmpTest.txt')) $expected.Add([System.IO.FileInfo]::new('Tmp Test.txt')) Mock Get-ChildItem { $arr = @( (New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'Tmp Test.txt'; CreationTime = [datetime]'2023-01-01 21:00:00' }), (New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'tmp-test.txt'; CreationTime = [datetime]'2023-01-01 22:00:00' }), (New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'TmpTest.txt'; CreationTime = [datetime]'2022-01-01 22:00:00' }), (New-MockObject -Type 'System.IO.FileInfo' -Properties @{ Name = 'qq02000.doc'; CreationTime = [datetime]'2020-01-01 22:15:00' }) ) # Mocking the -Filter parameter if used if ($PesterBoundParameters.Filter) { return $arr | Where-Object Name -Like $PesterBoundParameters.Filter } return $arr } $dt = [DateTime]::ParseExact('2023-01-01 00:00', 'yyyy-MM-dd HH:mm', $null) $list = CheckFilesToDelete -EnvPath $path -fList $folderList -date $dt -ErrorAction Stop $list.Count | Should -Be 2 $list[0].Name | Should -Be 'Tmp Test.txt' } } } ``` ## The result in PowerShell Finally, here the screenshot of Windows PowerShell SE and the output of the tests. ![Testing PowerShell scripts with Pester](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/05/image-4.png?resize=640%2C446&ssl=1)Testing PowerShell scripts with Pester## Wrap up In conclusion, this is a real example of testing PowerShell with Pester for a real world. I’m using this script in a production environment and the test with Pester is very useful. Please let me know what you think about. Happy coding! **Categories:** PowerShell **Tags:** pester, powershell, testing **Hashtags:** powershell, testing --- ### [How to sign up for Threads](https://puresourcecode.com/news/how-to-sign-up-for-threads/) **Published:** July 6, 2023 **Author:** Enrico **Excerpt:** In this new post, I show you how to sign up for Threads, the anti-Twitter by Meta. The new app is available from today in the US and the UK. **Content:** In this new post, I show you how to sign up for Threads, the anti-Twitter by Meta. ## The new Twitter by Meta Meta will officially release Threads, its text-based Twitter competitor, on Wednesday night, but for those who want to get ahead of the launch, there’s a way to access your invitation now. The social media giant, which owns Instagram and Facebook, will debut its new app days after Elon Musk said Twitter would temporarily limit the number of tweets users can read. The rate limits drove users to competing apps like Bluesky, the app backed by Twitter co-founder Jack Dorsey, which experienced “record-high traffic” on Saturday. While Bluesky remains in an invite-only beta phase, Meta doesn’t seem to have restrictions on who can use Threads, besides the requirement to have an Instagram account. Although the app is set to launch in the U.S. on Wednesday at 7 p.m. ET, according to the Threads update page, users based in the EU will reportedly have to wait. Here’s how you can get started. ## Open the Instagram app, search “Threads” Open the Instagram app and open the Explore page by clicking the magnifying glass icon in the bottom left. Type the word “Threads” into the search bar, and you should see a small ticket emoji appear on the far right. ![Search Threads on Instagram - Search Threads on Instagram](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0360.png?resize=472%2C1024&ssl=1)Search Threads on Instagram ## Tap the “Admit One” ticket in the search bar Click the “Admit One” ticket, and your Threads invitation should appear. The invite includes your username, the Threads launch date and time, a QR code, a numeric code and a “Get Threads” button at the bottom of the screen, which takes you to the App Store. Even though Threads is listed in the App Store, it’s not available for download yet. ![Invite to Threads from Instagram](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0361.png?resize=472%2C1024&ssl=1)Invite to Threads from Instagram![Invite to Threads from Instagram](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0362.png?resize=472%2C1024&ssl=1)Invite to Threads from Instagram## Open your profile, tap the three lines If you don’t see a ticket appear in the search bar, you can also access the invite by clicking the three lines on the top right of your profile. ![All options for your Threads account - How to sign up for Threads](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0363.png?resize=472%2C1024&ssl=1)All options for your Threads account – How to sign up for Threads![All options for your Threads account - How to sign up for Threads](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0364.png?resize=472%2C1024&ssl=1)All options for your Threads account – How to sign up for Threads **Categories:** News **Tags:** facebook, meta, social-media, twitter **Hashtags:** social-media, threads --- ### [Prepare a MAUI environment](https://puresourcecode.com/dotnet/visual-studio/prepare-a-maui-environment/) **Published:** July 4, 2023 **Author:** Enrico **Excerpt:** After my experience starting a new project and still feel the pain, here I show you How to prepare a MAUI environment for your projects **Content:** After my experience starting a new project and still feel the pain, here I show you How to prepare a [MAUI](https://puresourcecode.com/tag/maui/) environment for your projects. ## Table of contents - [Configure macOS and iOS](#h-configure-macos-and-ios) - [macOS](#macOS) - [Add an Apple account](#apple-account) - [Configure your devices](#ios-device) - [iPhone](#iphone) - [iPad](#ipad) - [Configure Visual Studio](#vs) - [The error](#error) - [Generate keys from Apple](#applekeys) - [Download and Store the Private Key](#3028598) - [Configure Android](#android) - [Add Android SDK](#sdk) - [Wrap up](#wrapup) ## Configure macOS and iOS Here the basic steps we follow in order to deploy an application using macOS to deploy to iPhone and iPad. As you know, you must have a macOS machine to build and deploy your applications to real devices or use the simulator. ### macOS The first step is the check on macOS is Xcode is installed. If not, you have to install from the [Apple](https://puresourcecode.com/category/news/apple/) Store. When you run Xcode, you should have a screenshot like the following. ![Xcode splash screen - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.09.05.png?resize=640%2C401&ssl=1)Xcode splash screen ### Add an Apple account The first action to do is to register your Apple account in Xcode and **Download Manual Profiles**. ![Register an Apple account in Xcode - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.20.32.png?resize=640%2C451&ssl=1)Register an Apple account in Xcode ### Configure your devices Now, from the menu, choose **Window** and then **Devices and Simulators**. ![Xcode Devices and Simulators - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.10.51.png?resize=640%2C915&ssl=1)Xcode Devices and Simulators In order to deploy an application on a real device, you have to register your device in Xcode from here. As you can see in the following screenshot, I want to pair my new iPhone. ![Pairing my iPhone to Xcode](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.19.22.png?resize=640%2C451&ssl=1)Pairing my iPhone to Xcode When the registration is completed and your device pairs with your macOS computer, you should see something like in the following screenshot. If it is your first registration, you won’t have any apps attached. ![Paired devices in Xcode - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.17.07.png?resize=640%2C451&ssl=1)Paired devices in Xcode #### Dummy test Now, the next step is create a dummy project to test is we can deploy on a real device. Choose from the first splash screen, the option **Create a new Xcode project**. ![Create a new dummy project in XCode](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.27.50.png?resize=640%2C428&ssl=1)Create a new dummy project in Xcode Then, select **App** from the list and press **Next**. If you try to run the application on your real device, you can get this error: > The operation couldn’t be completed. Unable to launch com.puresourcecode.dummy because it has an invalid code signature, inadequate entitlements or its profile has not been explicitly trusted by the user. > > Xcode ![Xcode couldn't run the dummy application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/Screenshot-2023-07-04-at-12.40.03.png?resize=640%2C428&ssl=1)Xcode couldn’t run the dummy application So, this is normal because you are not a trusted developers (basically you haven’t pay Apple for a certificate). For this reason, we have to move to the physical devices to accept yourself as a trusted developer. Note: you can do the next steps only if your device is registered in Xcode. If not, you won’t see the options for the **Developer mode**. ### iPhone Now, after the registration of your device in Xcode, you have to accept yourself as a trusted developer but before that you have to enable your device for the **Developer Mode**. #### Enable Developer Mode For that, go to the **Settings** of your device and look for **Privacy and Security**. If your device is connected to the macOS computer, you have the **Developer Mode** option. So, switch it **ON**. ![Developer Mode iPhone](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0347.png?resize=472%2C1024&ssl=1)Developer Mode iPhone #### Trust yourself Finally, you can trust yourself! Go the the **Settings** and then **General**. Here, you have the option **VPN & Device Management**. Click on that. ![iOS Settings - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0344.png?resize=472%2C1024&ssl=1)iOS Settings > General So, here you can see all the profiles that are installed in your device. Plus, a new entry for the **Apple Development**. This option is available only after the first failed deployment with Xcode (for this reason I created the dummy project earlier). ![iOS Settings > General > VPN & Device Management - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0345.png?resize=472%2C1024&ssl=1)iOS Settings > General > VPN & Device Management In the screenshot, you can see that my profile **Apple Development: enrico.rossini@me.com** (obviously you will have your account) under **Developer App** is not accepted yet. So, click on it and you see a new page where you can accept to trust yourself as a developer. ![Trust yourself - Prepare a MAUI environment](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_0346.png?resize=472%2C1024&ssl=1)Trust yourself So, now you can use your device to test your application for Xcode. ### iPad So, if you want to add your iPad, the process is exactly the same. Just put then screenshot for future reference. First, go the the **Privacy & Settings** and enable the **Developer Model** after the registration with your macOS computer. ![iPad Developer Mode](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_1031-1.png?resize=640%2C853&ssl=1)iPad Developer Mode Now, if you try to deploy and run the dummy project, you get the error on your macOS computer and also on your iPad. You see the error in the screenshot below. ![iPad couldn't trust the developer](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_1032.png?resize=640%2C853&ssl=1)iPad couldn’t trust the developer So, go to **General** and then **VPN & Device Management** and click on the **Apple Development** under **Developer App**. ![iPad VPN & Device management](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_1033.png?resize=640%2C853&ssl=1)iPad VPN & Device management Finally, you can click on **Trust Apple Deverloper**. ![iPad doesn't trust you as developer](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_1034.png?resize=640%2C853&ssl=1)iPad doesn’t trust you as developer After clicking on that, you have to **Trust** the developer. For this reason, you see a popup window that asks you explicitly to click on **Trust**. ![Accept to trust you as trusted developer](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/IMG_1035.png?resize=640%2C853&ssl=1)Accept to trust you as trusted developer Finally, your device is ready to receive deployment from your macOS machine. If you want to deploy your application from Visual Studio to a real device, you have to pay Apple. ## Configure Visual Studio If you want to deploy from your Visual Studio to your physical devices, you have to be registered as a developer on the Apple Store Connect. ### The error In case you don’t have a registration with the Apple Store Connect, you will receive this error: > Error Automatic Provisioning is enabled but no Development Team was selected. Please select a team or switch to Manual Provisioning from the iOS Bundle Signing page. > > Visual Studio If you don’t pay Apple, you will only be able to deploy from Visual Studio to a iOS Simulator. ### Generate keys from Apple To generate keys, you must have an Admin account in App Store Connect. You may generate multiple API keys with any roles you choose. To generate an API key to use with the App Store Connect API, log in to [App Store Connect](https://appstoreconnect.apple.com/). 1. Select Users and Access, and then select the API Keys tab. 2. Click Generate API Key or the Add (+) button. 3. Enter a name for the key. The name is for your reference only and is not part of the key itself. 4. Under Access, select the role for the key. 5. Click Generate. The new key’s name, key ID, a download link, and other information appears on the page. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/image-2.png?resize=640%2C295&ssl=1)### Download and Store the Private Key Once you’ve generated your API key, you are given the opportunity to download the private half of the key. The private key is available for download a single time. 1. Log in to [App Store Connect](https://appstoreconnect.apple.com/). 2. Select Users and Access, and then select the API Keys tab. 3. Click “Download API Key” link next to the new API key. The download link appears only if the private key has not yet been downloaded. Apple does not keep a copy of the private key. ## Configure Android ### Add Android SDK When Visual Studio starts to compile the project, an alert comes up because we have to accept the Android licence and install the version 31 of the Android API. So, accept the licence first. ![Android SDK - Licence Agreement - Exploring planets with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-3.png?resize=640%2C500&ssl=1)Android SDK – Licence Agreement After that, choose from the list of **Platforms** at least the **API Level 31** as Visual Studio required. ![Android Platforms to install for MAUI - Exploring planets with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-4.png?resize=640%2C512&ssl=1)Android Platforms to install for MAUI After the selection, you have to accept again the licence for the new platforms. ![Accept the Android licences - Exploring planets with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-5.png?resize=640%2C521&ssl=1)Accept the Android licences Now, compile again the project. It takes a while… Half an hour later, Visual Studio installed all the dependencies for the project. Now, in the Visual Studio toolbar, use the **Debug Target** drop down to select **Android Emulators** and then the **Android Emulator** entry ![Add an Android Emulator](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-7.png?resize=628%2C242&ssl=1)Add an Android Emulator In the toolbar press the **Android Emulator** button ![Start an Android Emulator](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-8.png?resize=640%2C45&ssl=1)Start an Android Emulator Accept again the licences 🤐and then in the **User Account Control** dialog, press the **Yes** button ![User Account Control dialog asks for Android SDK Manager](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-9.png?resize=428%2C311&ssl=1)User Account Control dialog asks for Android SDK Manager Then, accept again the Android licences (I’m already bored to create application with MAUI) and then in the Visual Studio toolbar, press the **Android Emulator** button ![Launch Android Emulator](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-10.png?resize=640%2C45&ssl=1)Launch Android Emulator Now, Visual Studio will start to create a default Android emulator. In the **User Account Control** dialog, press the **Yes** button ![User Account Control dialog asks for Android Device Manager](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-11.png?resize=428%2C312&ssl=1)User Account Control dialog asks for Android Device Manager In the New Device window, press the **Create** button ![Create a Default Android Device](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-12.png?resize=640%2C428&ssl=1)Create a Default Android Device Wait for Visual Studio to download, unzip, and create an Android emulator. After that, Close the **Android Device Manager** window ![Android Device Manager with the Default emulator](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-13.png?resize=640%2C401&ssl=1)Android Device Manager with the Default emulator In the Visual Studio toolbar, press the **Pixel 5 – API 30 (Android 11.0 – API 30)** button to build and run the app ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/07/image-14.png?resize=640%2C34&ssl=1) Visual Studio will start the Android emulator, build the app, and deploy the app to the emulator. After 10 minutes, the app is on the emulator, and I can click the button! ## Wrap up In conclusion, after all of it, we can finally deploy on a simulator, emulator or real device our application. If there is anything else I missed, please send me a message via the [Forum](https://puresourcecode.com/forum/). Based on what we have done, I’m going to create a new project for display advertisements in our applications and monetize with apps. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/07/image-1.png?resize=640%2C686&ssl=1)But this is the subject of the next post! Happy coding! **Categories:** Android, iOS, macOS, MAUI, Visual Studio **Tags:** android, ios, maui, visual-studio, windows11 **Hashtags:** maui --- ### [Testing PowerShell scripts with Pester](https://puresourcecode.com/tools/testing-powershell-scripts-with-pester/) **Published:** May 9, 2023 **Author:** Enrico **Excerpt:** I want to share with you an awesome way for testing PowerShell scripts with Pester, a framework for PowerShell script testing and mocking **Content:** In this post, I want to share with you an awesome way for testing PowerShell scripts with [Pester](https://github.com/pester/Pester), a framework for PowerShell script testing and mocking. The interesting part is that test scripts can work in a Azure pipeline. ## What is Pester? Pester is a testing and mocking framework for PowerShell. It provides a framework for writing and running tests. Pester is most commonly used for writing unit and integration tests, but it is not limited to just that. It is also a base for tools that validate whole environments, computer deployments, database configurations and so on. Pester follows a file naming convention `*.Tests.ps1`, and uses a simple set of functions: `Describe`, `Context`, `It`, `Should` and `Mock` to create a mini-DSL for writing your tests. Pester tests can execute any command or script that is accessible to a Pester test file. This includes functions, Cmdlets, Modules and scripts. Pester can be run locally, where it integrates well with Visual Studio Code, and it can of course be integrated into a build script in a CI pipeline. Pester contains a powerful set of Mocking capabilities that allow tests to replace the behavior of any command inside of a piece of PowerShell code being tested. See [Mocking with Pester](https://pester.dev/docs/usage/mocking). Pester can produce artifacts such as Test Results file and can be used for generating [Code Coverage](https://pester.dev/docs/usage/code-coverage) and [Test Result](https://pester.dev/docs/usage/test-results) files for reporting results in CI pipeline. ## Installing Pester[​](https://pester.dev/docs/quick-start#installing-pester) To install Pester it is usually enough to just do ``` Install-Module Pester -Force ``` Now, before seeing a real example, I like to spend few words about Windows PowerShell ISE. ## What is Windows PowerShell Integrated Scripting Environment (ISE)? The Windows PowerShell Integrated Scripting Environment (ISE) is a graphical user interface and front-end hosting application for Windows PowerShell. The ISE lets developers run PowerShell commands and create, test and refine PowerShell scripts without operating directly in the traditional PowerShell command-line interface (CLI). At first glance, PowerShell ISE is a convenient graphical user interface (GUI) for the PowerShell console. The ISE provides a variety of editing controls, user help and other ease-of-use features that aren’t readily present in PowerShell. For example, the ISE supports multi-line editing, tab completion, syntax-based coloring, selective execution, context-sensitive help and multi-language support. Menu options and keyboard shortcuts in the ISE mimic many of the common tasks traditionally performed in the PowerShell console. ## PowerShell ISE features A typical Windows 10 PowerShell ISE appears below. The conventional console area – the Console pane – is delineated in dark blue. A suite of familiar file and view controls are positioned along a top toolbar, including buttons to start a remote PowerShell session as well as a conventional PowerShell console. ![Windows PowerShell ISE - Testing PowerShell scripts with Pester](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/05/image-3.png?resize=640%2C383&ssl=1)Windows PowerShell ISE PowerShell ISE users can access help for the ISE by clicking the Help button in the top toolbar and selecting Windows PowerShell ISE Help. This opens a web page that offers descriptions and details of the PowerShell ISE and further reading. One key feature of the ISE is access to a complete library of PowerShell scripting language commands available from a command window located on the right. Developers can locate a command alphabetically and, by command group — which is filterable — drill down to enter all relevant parameters within the right panel. They can then insert the properly configured command into the console without the need to type the entire command by hand. A second core feature of ISE 2.0 is support for up to 32 concurrent execution environments. Previous versions of ISE supported only up to eight. This might seem like a great deal of multitasking. But developers can use this capability to work with related scripts and make real-time tweaks and enhancements while seeing the immediate effects of their changes across other related scripts. Other features of the ISE are related mainly to editing support. For example, the ISE supports multiline editing, allowing blank or new lines to be inserted beneath selected lines within the Command pane. Selective execution enables developers to run or test desired parts of the script by highlighting the desired portion of the script and clicking the Run Script button or pressing the F5 button. Similarly, users can add breakpoints to check variables and review script behaviors at critical points. Text copying and pasting is supported. A context-sensitive help system provides additional information about any item. The ISE itself has some customization options, and users can tailor text colors, fonts and layouts; add line and column number; and adjust keyboard shortcuts. Later versions of the PowerShell ISE add autocomplete capabilities for `cmdlets`, parameters, files and values. Autosave capabilities store the script every few minutes to avoid content loss if a crash occurs. The Snippets function saves short segments of code for reuse, and a most recently used list offers fast access to recent files. PowerShell ISE merges the command and output panes into a single view to more closely reflect the response of the PowerShell console. Users can extend the features and functionality of PowerShell ISE with code based on the ISE Scripting Object Model. ## Uses of PowerShell ISE The Windows PowerShell ISE is fundamentally an editing tool that is used to create, edit, test and execute PowerShell scripts in Windows environments. The ISE offers a more flexible and interactive editing and execution environment than a traditional PowerShell console. - **Save time and reduce errors when creating scripts.** Scripts routinely employ long sequences of complex command lines, each with granular parameters. It’s possible to construct the same script in PowerShell and the PowerShell ISE. But ISE features such as an interactive index of available commands and context-sensitive make it easy to find important commands, select proper parameters within the pane and then drop the properly formatted command into the script. This – along with other editing niceties such as copy and paste – can save time by speeding up proper command formatting as well as reducing common typing and syntactic errors that can be time-consuming to find and fix. - **Improve script debugging and testing.** A script is basically a short piece of software where the instructions involve the PowerShell scripting language. As with any software, there are bound to be errors, oversights and unintended consequences produced by the script. ISE features such as an integrated debugger and syntax highlighting can highlight common errors and recommend fixes before the script is ever executed. Additional capabilities such as breakpoints and selective execution let developers check desired portions of the script and intentionally stop execution at critical points in the script to inspect the state of important variables and other behaviours. - **Get greater insight into related scripts.** Scripts can be highly interactive entities where one script interacts with other scripts. This can result in complex relationships that can be difficult to follow with vanilla execution engines, such as PowerShell. The ISE supports multiple simultaneous execution environments, letting developers load and follow the cause-and-effect relationships among several scripts at the same time. The ISE helps with troubleshooting, especially after a change in one script triggers an unexpected behaviour or error in another. ### PowerShell vs. PowerShell ISE PowerShell and PowerShell ISE both provide fundamentally the same scripting capabilities for Windows environments. The principal difference between the two is convenience. PowerShell is a simpler and more straightforward scripting and execution environment, while the ISE provides more flexible and forgiving editing and execution features. PowerShell can be a good platform for simple tasks where actions are clear. The ISE is preferable when scripting tasks are larger, more complex and interrelated. A comparison of word processors offers a sound analogy. A tool such as Notepad can be ideal to create and edit notes and short, straightforward text. But a tool such as Word provides far more editing features, fonts, colours, formatting, and spelling and grammar checks. Thus, Word could be a preferable tool for complex tasks, such as professional report writing and developing a book chapter. Still, both tools are word processors. ### Advantages of PowerShell ISE To summarize, the advantages of PowerShell ISE include the following: - saved time; - fewer errors when creating scripts; - similar keyboard shortcuts replicating tasks possible in PowerShell; - improved debugging and testing; and - greater insight into related scripts. ### Disadvantages of PowerShell ISE The disadvantages of PowerShell ISE include the following: - unnecessary complexity for certain tasks; - lack of support for interactive sessions; - limited paging; - lack of support for certain legacy commands. ## How to run PowerShell ISE Windows PowerShell ISE is available in Windows 11, 10, 8.1, 8.0, and 7 as well as Windows Server 2008 R2 SP1 and later. PowerShell ISE can be launched on a PC in either one of two ways: - Click Start, search for PowerShell in the Search bar and then select Windows PowerShell ISE from the resulting list of apps. - Open the Windows Run prompt or any command shell, type powershell\_ise.exe and press Enter. Once the ISE is launched, users can employ the ISE in several common ways. - **Use the Console pane.** Once the ISE starts, it functions exactly like PowerShell, and users can enter commands into the Console pane – the large, dark blue area of the GUI – just as if it were PowerShell. For example, to run a command, just type the command into the Console pane at the command prompt and press Enter. Users can enter and execute multiple commands by using `Shift+Enter` – basically a line return – between commands. Users can stop the execution of a command with the Stop Operation button in the GUI or with `Ctrl+Break` on the keyboard. - **Create and use tabs.** PowerShell ISE 2.0 supports up to 32 simultaneous but independent execution environments or sessions. Each environment is referred to as a *tab*, and users can switch between tabs at-will. To create a new tab, click New PowerShell Tab on the File menu. Users can opt to create and use a remote PowerShell tab to establish a session on a remote computer, though this requires additional details to log in and access the remote computer. - **Manage breakpoints for debugging.** The ISE supports the use of breakpoints, which are points in the script where operation is paused for manual inspection of variables and environments. Once a breakpoint is encountered, the user can run commands to examine the state of the script, make changes to the state of the script and even resume operation of the script. Users can employ line breakpoints to pause at specific places, variable breakpoints to pause when a desired variable changes and command breakpoints to pause when a desired command is encountered. The ISE lets users set, remove and enable/disable breakpoints. - **Run a profile when the ISE starts.** A profile is a script that runs when a session is started. A profile can be vital to configure the PowerShell ISE environment for aliases, functions, variables, colors and fonts, and other preferences used in the ISE session or tab. Users can create, select, edit and enable/disable profiles in the ISE. - **Write and run scripts.** The core use for the ISE is to write, edit and run Windows PowerShell scripts. Script files can include conventional script files (.ps1), script data files (.psd1) and script module files (.psm1) as well as other files such as configuration files (.ps1xml), XML files and text files. To create a new script file, click New on the toolbar or click New on the File menu. The new empty file appears in a new file tab. Users can add commands and data to compose the script. To run the script, click Run Script on the toolbar or click Run on the File menu. To run just a part of the script, select or highlight the desired portion of the script and click Run Selection on the File menu or click Run Selection on the toolbar. **Categories:** PowerShell, Tools **Tags:** p, pester, powershell, powershell-ise **Hashtags:** powershell --- ### [Write a ChatGPT client](https://puresourcecode.com/dotnet/csharp/write-a-chatgpt-client/) **Published:** April 6, 2023 **Author:** Enrico **Excerpt:** Write a ChatGPT client in C# using the OpenAI API: walkthrough the process of creating human-like responses **Content:** In this new post, I explore how to write a ChatGPT client in C# using the [OpenAI](https://openai.com/) API. Also, walk through the process of creating a ChatGPT client in C# that can generate human-like responses to user queries. This ChatGPT client will also allow us to retain and send conversation history to the API, making it possible to maintain context and provide more personalized responses. I will also parameterize our ChatGPT client so that we can change whether or not to include the conversation history when calling the API, which language model to use, and the nature of the response. ![The ChatGPT client in action - Write a ChatGPT client](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/230140341-b9cab02e-8bcf-4672-b2e1-4c1853b20fd4.gif?w=640&ssl=1)The ChatGPT client in action As artificial intelligence continues to advance, language models such as ChatGPT have become increasingly powerful tools for natural language processing. ChatGPT is a large-scale neural language model that has been trained on massive amounts of text data and is capable of generating human-like responses to a wide variety of natural language prompts. It can be used for a range of applications, from chatbots and virtual assistants to language translation and content generation. So, to demonstrate the write a ChatGPT client in C#, I will be creating a chatbot in a console application using [C#](https://puresourcecode.com/category/dotnet/csharp/) and [NET 7](https://puresourcecode.com/category/dotnet/net7/) and [Spectre Console](https://puresourcecode.com/dotnet/net6/beautiful-console-applications-with-spectre-console/). The source code of this post is available on [GitHub](https://github.com/erossini/ChatGPTDemo). ## Obtain an API key First step, I have to obtain an API key from the OpenAI developer site. Go to the [OpenAI developer website](https://platform.openai.com/overview) and register yourself. ![OpenAI developer website - Write a ChatGPT client](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image-1.png?resize=640%2C653&ssl=1)OpenAI developer website After the successful registration, in your account on the top right, in the menu, you have **View API keys**. This option allows you to see the keys you already have or create a new one. ![View API keys - Write a ChatGPT client](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image-2.png?resize=508%2C653&ssl=1)View API keys For example, in my API keys I can see the one I have created for this demo. If you want to create a new key, click on the button **Create new secret key**. A window pop up will appear with a new key. Save it because it is not possible to read it again. ![OpenAI API keys view - Write a ChatGPT client](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image-3.png?resize=640%2C380&ssl=1)OpenAI API keys view ## ChatGPT has to retain the conversation Context Before creating our client, I will briefly look into the ChatGPT API’s default behaviour and how it influences the design of our ChatGPT client. Here is what happens if we don’t feed the API with all of the conversation’s previous messages: ![Image to show the behaviour of ChatGPT when it is not fed the conversation's previous messages. ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image-2.jpg?w=640&ssl=1) By the time we ask our second question, the API has already forgotten what the conversation is about. But here’s what happens when we feed the API with all of the conversation’s previous messages: ![Image to show the behaviour of ChatGPT when it is fed the conversation's previous messages.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image-3.jpg?w=640&ssl=1) By doing this, we are allowing the API to “remember” who “he” is and remember the context of the conversation. We can then design our ChatGPT C# client accordingly. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image-4.png?resize=640%2C226&ssl=1) ## The implementation First, I have to decide what version of the API I want to use. For this reason, I define an `enum` with the version available so far. ``` public enum OpenAIModels { gpt_4, gpt_4_0314, gpt_4_32k, gpt_4_32k_0314, gpt_3_5_turbo, gpt_3_5_turbo_0301 } ``` ### Response After that, I have to receive the response from ChatGTP and I refer to the [documentation](https://platform.openai.com/docs/api-reference/chat) to create the classes. ``` public class ChatResponse { public string? Id { get; set; } public string? Object { get; set; } public int Created { get; set; } public List? Choices { get; set; } public Usage? Usage { get; set; } } public class Choice { public int Index { get; set; } public Message? Message { get; set; } public string? Finish_Reason { get; set; } } public class Message { public string? Role { get; set; } public string? Content { get; set; } } public class Usage { public int Prompt_Tokens { get; set; } public int Completion_Tokens { get; set; } public int Total_Tokens { get; set; } } ``` ### The ChatGPTClient The *SendMessage* method is what we will call in order to generate our responses. This method calls the *Chat* method which sends our request to the API. Within the chat method, *GetMessageObjects* is called which retrieves either the current message only or all of the messages in the conversation. This allows the ChatGPT API to remember the context of the conversation. After the request is made, our message and the subsequent response are then saved to history using the *AddMessageToHistory* method. ``` public class ChatGPTClient { #region Variables private readonly string chatRequestUri; private readonly bool includeHistoryWithChatCompletion; private readonly List messageHistory; private readonly OpenAIModels model; private readonly string openAIAPIKey; private readonly double temperature; private readonly double top_p; #endregion Variables public ChatGPTClient(bool includeHistoryWithChatCompletion = true, OpenAIModels model = OpenAIModels.gpt_3_5_turbo, double temperature = 1, double top_p = 1) { chatRequestUri = "https://api.openai.com/v1/chat/completions"; openAIAPIKey = Environment.GetEnvironmentVariable("OpenAIAPIKey")!; messageHistory = new List(); this.includeHistoryWithChatCompletion = includeHistoryWithChatCompletion; this.model = model; this.temperature = temperature; this.top_p = top_p; } public async Task SendMessage(string message) { var chatResponse = await Chat(message); if (chatResponse != null) { AddMessageToHistory(new Message { Role = "user", Content = message }); foreach (var responseMessage in chatResponse.Choices!.Select(c => c.Message!)) AddMessageToHistory(responseMessage); } return chatResponse; } private void AddMessageToHistory(Message message) => messageHistory.Add(message); private async Task Chat(string message) { using var client = new HttpClient(); using var request = new HttpRequestMessage(HttpMethod.Post, chatRequestUri); request.Headers.Add("Authorization", $"Bearer {openAIAPIKey}"); var requestBody = new { model = GetModel(), temperature, top_p, messages = GetMessageObjects(message) }; request.Content = new StringContent(JsonSerializer.Serialize(requestBody), Encoding.UTF8, "application/json"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); if (response.IsSuccessStatusCode) { var chatResponse = await response.Content.ReadFromJsonAsync(); if (chatResponse != null && chatResponse.Choices != null && chatResponse.Choices.Any(c => c.Message != null)) return chatResponse; } return null; } private IEnumerable GetMessageObjects(string message) { foreach (var historicMessage in includeHistoryWithChatCompletion ? messageHistory : Enumerable.Empty()) { yield return new { role = historicMessage.Role, content = historicMessage.Content }; } yield return new { role = "user", content = message }; } private string GetModel() => model.ToString().Replace("3_5", "3.5").Replace("_", "-"); } ``` **Categories:** .NET7, C# **Tags:** artificial-intelligence, csharp, openai --- ### [Beautiful console applications with Spectre.Console](https://puresourcecode.com/dotnet/net6/beautiful-console-applications-with-spectre-console/) **Published:** April 5, 2023 **Author:** Enrico **Excerpt:** Using the new NuGet package, we can create beautiful console applications with Spectre.Console that are not boring or monochrome **Content:** Using the new [NuGet](https://www.nuget.org/packages/spectre.console) package, we can create beautiful [console](https://puresourcecode.com/?s=console) applications with [Spectre.Console](https://spectreconsole.net/) that are not boring or monochrome. The repository of this project is on [GitHub](https://github.com/spectreconsole/spectre.console). The repository of this demo is on [GitHub](https://github.com/erossini/SpectreConsoleDemo) too. Sure, you can use `Console.WriteLine` and `Console.ReadLine` to output some text and get some input, but that’s pretty boring and limited. What if you want to display some colors, styles, tables, trees, progress bars, or even ASCII images? ![Beautiful console applications with Spectre.Console - Demo](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/230061067-4b0f5a30-cfe8-4d03-9e6f-1fd76bfa0b37.gif?w=640&ssl=1) ## What is Spectre.Console? `Spectre.Console` is a .NET [library](https://www.nuget.org/packages/spectre.console) that makes it easier to create beautiful console applications. It is heavily inspired by the excellent Rich library for Python written by Will McGugan. Also, ut supports 3/4/8/24-bit colors in the terminal with auto-detection of the current terminal’s capabilities. This library provides a rich markup language that lets you easily output text with different colors and styles such as bold, italic, and blinking. So grab a cup of coffee and let’s dive in! ## Installing Spectre.Console To install `Spectre.Console`, you need to use NuGet Package Manager. You can either use Visual Studio or the dotnet CLI tool. If you are using Visual Studio, right-click on your project and select Manage NuGet Packages. Then search for `Spectre.Console` and install it. If you are using dotnet CLI tool, run this command in your project folder: ``` dotnet add package Spectre.Console ``` ## Using Spectre.Console To use `Spectre.Console`, you need to import its namespace: ``` using Spectre.Console; ``` Then you can access its main class `AnsiConsole`, which provides various methods for outputting text and rendering widgets. For example, here is how you can output some text with different colors and styles: ``` AnsiConsole.MarkupLine("[bold green]Hello[/] [italic blue]World[/]!"); ``` Also, you can use RGB or HEX values for specifying colors: ``` AnsiConsole.MarkupLine("This is [rgb(255;0;0)]red[/], this is [rgb(0;255;0)]green[/], this is [rgb(0;0;255)]blue[/]."); AnsiConsole.MarkupLine("This is [#ff0000]red[/], this is [#00ff00]green[/], this is [#0000ff]blue[/]."); ``` You can also nest tags for combining colors and styles: ``` AnsiConsole.MarkupLine("[bold red on yellow blink underline]Warning![/] This is very [italic green on black strikethrough]important[/]."); ``` ## Rendering widgets `Spectre.Console` also provides various widgets that you can render in your console applications. Some of them are: - **Tables**: Display tabular data with customizable headers, footers, borders, and alignment. - **Trees**: Display hierarchical data with expandable nodes and icons. - **Bar Chart**: Display a horizontal bar chart on the console. - **Progress**: Display progress for long-running tasks with live updates of progress bars and status controls. - and many others Here are some examples of how to render these widgets: ### Tables To render a table, you need to create an instance of `Table` class and add some columns and rows. You can also customize its appearance by setting properties such as `BorderStyle`, `Border`, `Title`, `Caption`, etc. For example, ``` var table = new Table(); table.AddColumn("Name"); table.AddColumn("Age"); table.AddColumn("Occupation"); table.AddRow("Alice", "23", "Software Engineer"); table.AddRow("Bob", "32", "Accountant"); table.AddRow("Charlie", "28", "Teacher"); table.Title = new TableTitle("[underline yellow]People[/]"); table.Caption = new TableTitle("[grey]Some random people[/]"); AnsiConsole.Write(table); ``` You can find more details about the table widget here: ### Trees To render a tree, you need to create an instance of `Tree` class and add some nodes. You can also customize its appearance by setting properties such as `Style`, `Guide`, `Expand`, etc. For example, ``` var tree = new Tree("[yellow]Root[/]"); var child1 = tree.AddNode(new Markup("[green]Child 1[/]")); var child2 = tree.AddNode(new Markup("[green]Child 2[/]")); var child3 = tree.AddNode(new Markup("[green]Child 3[/]")); child1.AddNode("[blue]Grandchild 1-1[/]"); child1.AddNode("[blue]Grandchild 1-2[/]"); child2.AddNode("[green]Grandchild 2-1[/]"); var grandchild3 = child3.AddNode("[green]Grandchild 3-1[/]"); child3.AddNode("[green]Grandchild 3-2[/]"); grandchild3.AddNode("[yellow]Great Grandchild 3-1-1[/]"); grandchild3.AddNode("[yellow]Great Grandchild 3-1-2[/]"); AnsiConsole.Write(tree); ``` You can find more details about the tree widget here: ### Progress To render progress, you need to create an instance of `Progress` class and add some tasks. You can also customize its appearance by setting properties such as `AutoClear`, `AutoRefresh`, `Columns`, etc. For example, ``` await AnsiConsole.Progress() .StartAsync(async ctx => { // Define tasks var task1 = ctx.AddTask("[green]Chrome RAM usage[/]"); var task2 = ctx.AddTask("[yellow]VS Code RAM usage[/]"); while (!ctx.IsFinished) { // Simulate some work await Task.Delay(100); // Increment task1.Increment(4.5); task2.Increment(2); } }); ``` You can find more details about the progress widget here: ### Bar Chart To render a bar chart, you need to create an instance of BarChart class and add some items with labels, values, and colors. You can also customize its appearance by setting properties such as Width, Label, CenterLabel, etc. ``` // Create a bar chart AnsiConsole.Write(new BarChart() .Width(60) // Set the label of the chart .Label("[green bold underline]Global Smartphone Shipments Market Share (%)[/]") //And center it .CenterLabel() // Add the items with lables, values, and colors .AddItem("Apple", 23, Color.Yellow) .AddItem("Samsung", 19, Color.Green) .AddItem("Xiaomi", 11, Color.Red) .AddItem("OPPO", 10, Color.Blue) .AddItem("Vivo", 8, Color.DarkMagenta) .AddItem("Others", 29, Color.Orange1)); ``` ## Using Live Display `Spectre.Console` can update arbitrary widgets in place using the [Live Display](https://spectreconsole.net/api/Spectre.Console/LiveDisplay) widget. This can be useful for creating dynamic tables that show changing data over time. The live display is not thread-safe, and using it together with other interactive components such as prompts, status displays, or other progress displays is not supported. To render a live table, you need to create a Table instance and add some columns and rows. Then you need to pass the table to `AnsiConsole.Live()` method and call `Start()` or `StartAsync()` with an action or a function that updates the table content. You can use `ctx.Refresh()` to refresh the display after each update. ``` // Create a table var table = new Table() .Border(TableBorder.Rounded) .AddColumn("Id") .AddColumn("Name") .AddColumn("Age"); // Add some initial rows table.AddRow(Faker.Identification.SocialSecurityNumber(), Faker.Name.First(), Faker.RandomNumber.Next(18, 99).ToString()); table.AddRow(Faker.Identification.SocialSecurityNumber(), Faker.Name.First(), Faker.RandomNumber.Next(18, 99).ToString()); // Use LiveDisplay to update the table await AnsiConsole.Live(table) .StartAsync(async ctx => { // Loop until we are done for (int i = 0; i < 5; i++) { var id = Faker.Identification.SocialSecurityNumber(); var name = Faker.Name.First(); var age = Faker.RandomNumber.Next(18, 99); table.AddRow(id, name, age.ToString()); ctx.Refresh(); // Simulate doing the work await Task.Delay(1000); } }); ``` ### Faker.net In this piece of code, I use [Faker.net](https://github.com/oriches/faker-cs) for generating random values. Available as a [NuGet](https://nuget.org/packages/Faker.Net) package. C# port of the Ruby Faker gem () and is used to easily generate fake data: - addresses (UK, US), - boolean, - companies, - countries, - currencies, - enums, - finance (isin, ticker, coupon, maturity, bond name), - identification (social security number (US), MBI (US), national insurance number (UK), passport number (UK & US), Bulgarian Person Identification Number(PIN/ENG)) - internet (email, domain names, user names), - lorem ipsum, - names, - phone numbers Here an example of what this library can generate: ``` var name = Faker.Name.FullName(); // Tod Yundt var firstName = Faker.Name.First(); // Orlando var lastName = Faker.Name.Last(); // Brekke var address = Faker.Address.StreetAddress(); // 713 Pfeffer Bridge var city = Faker.Address.City(); // Reynaton var number = Faker.RandomNumber.Next(100); // 30 var dob = Faker.Identification.DateOfBirth(); // 1971-11-16T00:00:00.0000000Z // US - United States var ssn = Faker.Identification.SocialSecurityNumber(); // 249-17-9666 var mbi = Faker.Identification.MedicareBeneficiaryIdentifier(); // 8NK0Q74KT53 var usPassport = Faker.Identification.UsPassportNumber(); // 335587506 // UK - United Kingdom var nin = Faker.Identification.UkNationalInsuranceNumber(); // YA171053Y var ninFormatted = Faker.Identification.UkNationalInsuranceNumber(true); // YA 17 10 53 Y var ukPassport = Faker.Identification.UkPassportNumber(); // 496675685 var ukNhs = Faker.Identification.UkNhsNumber(); // 6584168301 var ukNhsFormatted = Faker.Identification.UkNhsNumber(true); // 658 416 8301 // BG - Bulgaria var bulgarianPin = Faker.Identification.BulgarianPin(); //6402142606 ``` **Categories:** .NET6 **Tags:** console, dotnet --- ### [Custom JavaScript function in Blazor](https://puresourcecode.com/dotnet/blazor/custom-javascript-function-in-blazor/) **Published:** April 3, 2023 **Author:** Enrico **Excerpt:** In this new post custom JavaScript function in Blazor, I present how to create functions in C# in a Blazor page and integrate them with JavaScript **Content:** In this new post custom [JavaScript](https://puresourcecode.com/tag/javascript/) function in [Blazor](https://puresourcecode.com/tag/blazor/), I present how to create functions in [C#](https://puresourcecode.com/category/dotnet/csharp/) in a Blazor page and integrate them with JavaScript. What I explain in this post, it is coming fro my [ChartJs component for Blazor](https://puresourcecode.com/dotnet/blazor/blazor-component-for-chartjs/): this component allows you to create nice graphs using [ChartJs](https://www.chartjs.org) library. ## Scenario The ChartJs library allows the developers to customise the chart using `callbacks` and here developers can write their own JavaScript code. For example, if you want to customise the `Tooltip` for each point, you can use the `Tooltip Callbacks` ``` const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y); } return label; } } } } } }); ``` So, if you look at the lines 6-18, the `callbacks` has a property `label` and here we can write our JavaScript code. Then, when the graph renders the `tooltip` for a point, the library calls the `callbacks` function and displays the label in the format you define. Here the screenshot for the code above. ![ChartJs custom tooltip - Custom JavaScript function in Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/Blazor-ChartJs-custom-tooltip.png?resize=640%2C327&ssl=1) Here, you can try yourself with this [Codepen](https://codepen.io/erossini/pen/ExeqXdz). When you move the mouse over a point, the tooltip is displaying the custom label. CodePen Embed Fallback So, the problem is how to replicate the same functionalities in the Blazor component and allow developers to create their custom code. Then, I was looking for an implementation where I can add in a Blazor page the JavaScript code and call it in some way (see my first post in the [Microsoft Learn](https://learn.microsoft.com/en-us/answers/questions/852437/how-can-i-link-a-custom-function-to-a-chartjs-via)). Nothing was working. I couldn’t understand how to pass data and communicate among the JavaScript code, the Blazor component and the Blazor Page. Most of the people redirected me to the Microsoft Learn page called “[Call JavaScript functions from .NET methods in ASP.NET Core Blazor](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0)” but wasn’t useful. After few months, a pull request on [GitHub](https://github.com/erossini/BlazorChartjs) from [Macias](https://github.com/macias) opened my mind. ## Anatomy of the component First, just few words about the ChartJs component for Blazor and how it is working. Obviously this is an example to implement your custom JavaScript function in your Blazor components and projects. ![Anatomy of the component - Custom JavaScript function in Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/04/image.png?resize=640%2C419&ssl=1)Anatomy of the ChartJS component for Blazor So, I’m telling you all this stuff to explain my thoughts about the component. But also, how I discovered the correct implementation. ### Chart.js script First, the `Chart.js` script is the core of all component. As I said, this is based on the javascript library ChartJs. So, the component is a faced of it adding the ability to use it in Blazor applications. So, this script calls and communicates with the ChartJs library to create the chart. The component exposes the properties, methods and events to developers in their Blazor pages. The real definition of the settings are coming from the Blazor page. Then, the script initialises the chart calling the ChartJS library. In this script, there are other functions available out-of-the-box, such as the [crosshair](https://chartjs.puresourcecode.com/crosshair). Also, the component raises events and calls to the Blazor component. For example, when the user clicks on the chart, the Chart.js invoke `ChartClick` in the Blazor component. ### Blazor component In order to create the chart, the script has to receives the configuration of the chart from the Blazor component. The component is simplified the creation exposing properties, events and methods that developers can use in their applications. So, in same way I have to add here something that developers can use to add their code in C#. ### Blazor page Finally, this is the page where we want to generate the chart, passing to the component all the settings. Also, in here I want to add my custom code to change the chart. ## What I need to do So, focus on the `Tooltip callbacks`, what I need is: - the JavaScript has to call the Blazor component for a particular event (in this case when there is a callback during the creation of a tooltip) - the Blazor component must to receive the call or connect the code between the JavaScript code and the custom code in the page - the Blazor component must call the custom code, pass parameters if it needs them and sends back the result ## Implementation in the component Now, we know the structure of the component and what I want to do. So, we can start to analyse how to implement the solution. First, in the component I define ``` private DotNetObjectReference? dotNetObjectRef; ``` As the name says, this is a reference of my chart configuration that has all the data, labels and options. When the `Chart.js` calls a function in this component, it will pass always this object to access the data and options. Then, I have to define a function that Chart.js can call. For that, I have to define a `JSInvokable` function like that ``` [JSInvokable] public static string[] TooltipCallbacksLabel( DotNetObjectReference config, int[] parameters) { var ctx = new CallbackGenericContext(parameters[0], parameters[1]); if (config.Value.Options is Options options) return options.Plugins.Tooltip.Callbacks.Label(ctx); else throw new NotSupportedException(); } ``` So, what is it happing here? The magic Is here. The `Chart.js` calls `TooltipCallbacksLabel` with 2 parameters: the configuration of the chart and some `parameters` that in this particular case are the value of a specific point in the chart. In the `config`, the component checks if the `Options` are defined and if there is a particular implementation for the `Callbacks.Label`. If there is one, it pass the values to the function. Basically, this is how the Blazor component calls a custom C# code in the Blazor pageant return to the `Chart.js` the result. In this case, the result of the C# is a string. ### In the Blazor page So, let me show you the implementation in the Blazor page. ``` protected override async Task OnInitializedAsync() { _config1 = new BarChartConfig() { Options = new Options() { Responsive = true, MaintainAspectRatio = false, Plugins = new Plugins() { Legend = new Legend() { Align = Align.Center, Display = true, Position = LegendPosition.Right }, Tooltip = new Tooltip() { Callbacks = new Callbacks() { Label = (ctx) => { return new[] { $"DataIndex: {ctx.DataIndex}\nDatasetIndex: {ctx.DatasetIndex}" }; }, Title = (ctx) => { return new[] { $"This is the value {ctx.Value}" }; } } } }, Scales = new Dictionary() { { Scales.XAxisId, new Axis() { Stacked = true, Ticks = new Ticks() { MaxRotation = 0, MinRotation = 0 } } }, { Scales.YAxisId, new Axis() { Stacked = true } } } } }; ``` Look at the lines between 17 and 30. Here is where the Blazor page receives from the Blazor component the values from the `Chart.js` script. This code creates a `string[]` to change the text in the tooltip for a particular point. So, there is a constant communication between the Blazor component and the JavaScript underneath. When it is the code, the Blazor component pass the control to the Blazor page and then the result to the JavaScript. ### In the JavaScript And what is the code in the `Chart.js`? In the configuration of the chart, I pass the `DotNetObjectReference`. When the ChartJs library wants to call the `label` `callbacks`, there is a function that invoke the `JSInvokable` function I defined in the Blazor component. The call is using the namespace of the component (in this case `PSC.Blazor.Components.Chartjs`) and the function I want to call (in this case `TooltipCallbacksLabel`). ``` config.options.plugins.tooltip.callbacks.label = function (ctx) { return DotNet.invokeMethod('PSC.Blazor.Components.Chartjs', 'TooltipCallbacksLabel', dotnetConfig, [ctx.datasetIndex, ctx.dataIndex]); }; ``` As parameters, the function pass to the Blazor component the chart configuration and an array with some values (in this case `datasetIndex` and `dataIndex` available in this function). So, the Blazor component pass the parameters to the custom code in the Blazor page, receives the new string and pass the string to the `Chart.js` that pass the value to the ChartJs library. ## Wrap up I took almost a year to understand how to create custom JavaScript function in a Blazor component that can communicate among them. I couldn’t find any documentation that explain how to do it. So, I hope this can help someone else. For more info, you can visit: - the [GitHub](https://github.com/erossini/BlazorChartjs) repository for ChartJs component t for Blazor - [ChartJs Blazor component for Blazor](https://puresourcecode.com/dotnet/blazor/blazor-component-for-chartjs/) post - [Labels and OnClickChart for ChartJs](https://puresourcecode.com/dotnet/blazor/labels-and-onclickchart-for-chartjs/) - the [forum](https://puresourcecode.com/forum/chart-js-for-blazor/) for this component - download the [NuGet](https://www.nuget.org/packages/PSC.Blazor.Components.Chartjs/) package - [play with the demo](https://chartjs.puresourcecode.com) Happy coding! **Categories:** Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly **Hashtags:** blazor --- ### [Working with Blazor’s component model](https://puresourcecode.com/dotnet/csharp/working-with-blazors-component-model/) **Published:** April 23, 2021 **Author:** Enrico **Content:** Welcome to Working with Blazor’s component model” post! In this new post I’ll build a simple project in [Blazor](https://puresourcecode.com/?s=blazor) and explain the basic [Blazor](https://puresourcecode.com/?s=blazor) components and interactions. Also, the source code of the project I’m going to create in this post is available on [GitHub](https://github.com/erossini/BlazorTrails). Here the posts I wrote about Blazor that help you to learn this new technology better and faster: - [Getting Started With C# And Blazor](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/) - [Setting up a Blazor WebAssembly application](https://puresourcecode.com/dotnet/blazor/setting-up-a-blazor-webassembly-application) - [Working with Blazor’s component model](https://puresourcecode.com/dotnet/blazor/working-with-blazors-component-model/) ## Table of contents - [What is a Blazor component?](#h-what-is-a-blazor-component) - [Structuring Components](#sigil_toc_id_25) - [Single file](#sigil_toc_id_26) - [HomePage code](#h-homepage-code) - [Code explained](#h-code-explained) - [Visual Studio and Razor](#h-visual-studio-and-razor) - [Partial class](#sigil_toc_id_27) - [Pros of separating UI and logic](#h-pros-of-separating-ui-and-logic) - [Cons of separating UI and logic](#h-cons-of-separating-ui-and-logic) - [Component lifecycle methods](#sigil_toc_id_28) - [The first render](#sigil_toc_id_29) - [Render explained](#h-render-explained) - [The lifecycle with async](#sigil_toc_id_30) - [Dispose – The extra lifecycle method](#sigil_toc_id_31) - [Communicating between parent and child components](#sigil_toc_id_32) - [Passing values from a parent to a child](#sigil_toc_id_33) - [The TrailDetails component](#heading_id_3) - [Define a component parameter](#h-define-a-component-parameter) - [Drawer implementation](#h-drawer-implementation) - [Updating the HomePage component](#heading_id_4) - [Passing data from a child to a parent](#sigil_toc_id_34) - [Code explained](#h-code-explained-1) - [Styling components](#sigil_toc_id_35) - [Global styling](#sigil_toc_id_36) - [Why a global CSS?](#h-why-a-global-css) - [Scoped styling](#sigil_toc_id_37) - [Example of scoped styling](#h-example-of-scoped-styling) - [Global styles can still have an effect](#heading_id_5) - [Using CSS preprocessors](#sigil_toc_id_38) - [Integrating a CSS preprocessor](#heading_id_6) - [Integrate tools](#h-integrate-tools) - [Conclusion](#h-conclusion) ## What is a Blazor component? First, the fundamental building blocks of [Blazor](https://puresourcecode.com/?s=blazor) applications are components, almost everything you do will directly or indirectly work with them. In order to build great applications, you must know how to harness their power. So, components define a **piece of UI**, that can be something as small as a button or as large as an entire page, components can also contain other components. They encapsulate any data that that piece of UI requires to function. They allow a piece of UI that you can reuse across an application or even shared across multiple applications. Then, you can pass data into a component using **parameters**. Parameters **define the public API of a component**. The syntax for passing data into a component using parameters is like defining attributes on a standard HTML element, with a key/value pair. The key being the parameter name and the value being the data you wish to pass to the component. Also, the data a component holds is more commonly referred to as its *state*. Methods on a component define its logic. These methods manipulate and control that state via the handling of events. Also, we can style the components via traditional global styling; however, it is more common to use scoped styles. Scoped styles allow the component to define its own CSS classes without fear of collision with other styles in the application. It’s even possible to use CSS pre-processors such as Sass with scoped styling. ![The new component for drawer in our application - Working with Blazor’s component model](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-15.png?resize=640%2C560&ssl=1)The new component for drawer in our application Now, we’ll be adding a cool new slide out drawer feature to Blazor Trails. The drawer will slide out from the right-hand side of the page when the user clicks on a button we’ll add to the `TrailCard` component. The drawer will display more detailed information about the selected trail. When the user clicks the close button on the drawer it will cleanly slide back out of view. ## Structuring Components As you will find with almost every part of Blazor, there are multiple ways of doing things. The Blazor team have been very deliberate with making the framework unopinionated. So, developers can build applications the way that works best for them. It is possible to define a component in a single .razor file that contains both its markup and logic. Also, it is possible to separate a component into a `.razor` file which defines the markup and a [C#](https://puresourcecode.com/category/dotnet/csharp/) class which defines the logic. ### Single file When using a single file approach all mark-up and logic for a component is defined in a single file. The primary advantage of this approach is that it allows you to work with everything in one place. This can really help with productivity as you don’t need to keep swapping back and forth between multiple files. Single file is the default when creating new components. #### HomePage code ``` @page "/" @inject HttpClient Http @if (_trails == null) { Loading trails... } else { @foreach (var trail in _trails) { } } @code { private IEnumerable _trails; protected override async Task OnInitializedAsync() { try { _trails = await Http.GetFromJsonAsync("trails/trail-data.json"); } catch (HttpRequestException ex) { Console.WriteLine($"There was a problem loading trail data: {ex.Message}"); } } } ``` ### Code explained The code should look familiar, this is the Blazor Trails home page component. The entire component is defined in a single `.razor` file with the markup coming first then the logic coming second, defined in the code block. So, having everything in a single file, it allows me to work faster as I don’t have to switch files. But there is another benefit which I find useful, monitoring component size. When building applications, it’s easy to create very large components which are doing lots of things. However, just like when creating regular C# classes, you should try to keep your components focused, with a single purpose. One way I use to gauge this is the size of my component files. When I find I’m constantly scrolling up and down a file adding markup and logic, it’s an indication that my component may be doing too much and I should be thinking about splitting it out into additional components with more focused responsibility. This isn’t a clear-cut method however, there are times when a component may be quite large but still only have one responsibility, but it at least makes me think about it. One argument I often hear against this method is that mark-up and logic should be separated because otherwise we’re mixing concerns. I disagree with this view. The logic in a component should be logic which operates over the mark-up and drives the function of the component. **Business logic has no place in components**. If you take this view then the logic and mark-up are intertwined, they are tightly coupled, one can’t exist without the other. In which case separating them seems to fall into the same category as organizing an applications files by type rather than feature, and this is inefficient and hinders productivity. #### Visual Studio and Razor There is one drawback of this approach at the moment, and that is the functionality of the Razor editor in Visual Studio. There are several key refactoring abilities which don’t work in razor files, namely quick actions and refactorings. Renaming of variables can be a bit sketchy and only work some of the time. This is due to the fact the Razor editor in Visual Studio is not fully support Blazor yet. However, the tooling teams are currently completely rebuilding the Razor tooling experience from the ground up to include all the rich functionality that developers have come to expect when working with regular C# class files. Once this work is complete, the experience working in Razor files should be on par with that of C# class files. ### Partial class Another approach is to split the markup and logic of a component into two separate files. The markup of the component is kept in the .razor file, the logic is added to a C# class. In earlier versions of Blazor it was only possible to apply this approach using inheritance as there was no support for the partial keyword, this is no longer the case. Let’s take a look at the home page component refactored to use this approach. ``` @page "/" @if (_trails == null) { Loading trails... } else { @foreach (var trail in _trails) { } } ``` Now the logic for the component. ``` using Microsoft.AspNetCore.Components; using System; using System.Collections.Generic; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; namespace BlazorTrails.Features.Home { public partial class HomePage : ComponentBase { private IEnumerable _trails; [Inject] public HttpClient Http { get; set; } protected override async Task OnInitializedAsync() { try { _trails = await Http.GetFromJsonAsync("trails/trail-data.json"); } catch (HttpRequestException ex) { Console.WriteLine($"There was a problem loading trail data: {ex.Message}"); } } } } ``` As you can see, using this technique you can make the two elements of the component completely separate. You should also note the naming of the files, `HomePage.razor` and `HomePage.razor.cs`. If you’re using Visual Studio to build your applications, following this naming convention will produce a nested effect. ![ Naming a partial class the same as the markup portion of the component will produce a nested effect when using Visual Studio - Working with Blazor’s component model](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-16.png?resize=274%2C371&ssl=1) Naming a partial class the same as the markup portion of the component will produce a nested effect when using Visual Studio #### Pros of separating UI and logic This makes it easier to work with the component. It also keeps the number of files being displayed in the IDE to a minimum as you can simply hide any partial classes you’re not currently interested in. The major benefit of separating the markup and logic of the component is the development experience. As I mentioned when talking about the single file approach, the razor editor is not as fully featured as when working with regular C# class files. By separating out the logic to a C# class file, developers can access all the editor features. #### Cons of separating UI and logic The drawback of this approach is that you now have two files to manage when you’re working with a component. This can end up with lots of switching back and forth as you will need to be in the logic file when adding methods or other members to the component. Then you will need to be in the mark-up file to add any UI, hook up event handlers, etc. Largely which approach you choose for building your applications is a personal choice based on which method you find most productive. ## Component lifecycle methods Just as in other component-based frameworks, components in Blazor have a lifecycle. ![Blazor component lifecycle methods - Working with Blazor’s component model](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/component-lifecycle-1.jpg?w=640&ssl=1)Blazor component lifecycle methods Depending on what an application is doing, it may need to perform actions at certain points during this lifecycle. For example, load initial data for the component to display when it is first created, or update the UI when a parameter has a certain value from the parent. Blazor supports this by giving us access to the component lifecycle at specific points. 1. Initialized component: `OnInitialized`/`OnInitializedAsync` 2. Parameter set:` OnParametersSet`/`OnParametersSetAsync` 3. After Render: `OnAfterRender`/`OnAfterRenderAsync` The lifecycle methods are provided by the `ComponentBase` class which all components inherit from. Each method has a synchronous and asynchronous version. The synchronous version is always called before the asynchronous version. ``` Componet Lifecycle Check the browser console for details... @code { public override async Task SetParametersAsync(ParameterView parameters) { Console.WriteLine("SetParametersAsync - Begin"); await base.SetParametersAsync(parameters); Console.WriteLine("SetParametersAsync - End"); } protected override void OnInitialized() { Console.WriteLine("OnInitialized"); } protected override async Task OnInitializedAsync() { Console.WriteLine("OnInitializedAsync"); } protected override void OnParametersSet() { Console.WriteLine("OnParametersSet"); } protected override async Task OnParametersSetAsync() { Console.WriteLine("OnParametersSetAsync"); } protected override void OnAfterRender(bool firstRender) { Console.WriteLine($"OnAfterRender (First render: {firstRender})"); } protected override async Task OnAfterRenderAsync(bool firstRender) { Console.WriteLine($"OnAfterRenderAsync (First render: {firstRender})"); } } ``` ### The first render During the first render, all the component’s lifecycle methods will be called – during subsequent renders only a subset of the methods will run. The process starts with `SetParametersAsync` being called. This is the only lifecycle method which requires us to call the base method, if we don’t then the component will fail to load. This is because the base method does two essential things: - Sets the values for any parameters the component defines – This happens both the first time the component is rendered and whenever parameters could have changed - Calls the correct lifecycle methods depending if the component is running for the first time or not If we removed the call to the base method the output in the browser console would look like this. #### Render explained During a first render, the component hasn’t been initialized. This means that `OnInitialized` and `OnInitializedAsync` will be called first – it is also the only time they will run. This pair of methods are the only ones which run once in a component’s lifetime. You can think of these as constructors for your component. It makes them a great place to make API calls, for example, to get the initial data the component will display. Once the `OnInitialized` methods have run, `OnParametersSet` and `OnParametersSetAsync` are called. These methods allow developers to perform actions whenever a components parameters change. In the case of a first render, the components parameters have been set to their initial values. The final methods to run are `OnAfterRender` and `OnAfterRenderAsync`. These methods both take a `Boolean` value indicating if this is the first time the component has been rendered. On the initial render, the value of `firstRender` will be set to `true` for every render after, it will be `false`. ``` void OnAfterRender(bool firstRender) Task OnAfterRenderAsync(bool firstRender) ``` This is useful as it allows one-time operations to be performed when a component is first rendered, but not on subsequent renders. The primary use of the `OnAfterRender` methods are to perform JavaScript interop and other DOM related operations such as setting the focus on an element. ### The lifecycle with async One key point about the render we just covered was that it ran synchronously. In the Lifecycle component there are no awaited calls in any of the `async` lifecycle methods, meaning each method ran in sequence. However, when `async` calls are added then things look a bit different. To demonstrate this let’s update the `OnInitializedAsync` method in the `Lifecycle.razor` component to make an async call. ``` protected override async Task OnInitializedAsync() { Console.WriteLine("OnInitializedAsync - Begin"); await Task.Delay(300); Console.WriteLine("OnInitializedAsync - End"); } ``` While Blazor was awaiting the `async` call, the component was rendered. You can see it was then rendered a second time after the `OnParametersSet` methods, as before. This is because Blazor checks to see if an awaitable task is returned from `OnInitializedAsync`, if there is it calls `StateHasChanged` to render the component with the results of any of the asynchronous code which has been run so far, while awaiting the completion of the task. This behavior is also true for `async` calls made in `OnParametersSetAsync`. ### Dispose – The extra lifecycle method There is another lifecycle method which we can use but this one is optional and it’s not built-in to the ComponentBase class, `Dispose`. This method is used for the same purposes in Blazor as in other C# applications, to clean up resources. This method is essential when creating components which subscribe to events, as failing to unsubscribe from events before a component is destroyed will cause a memory leak. In order to access this method a component must implement the `IDisposable` interface. ``` @implements IDisposable Componet Lifecycle Check the browser console for details... @code { // Other methods ommitted for brevity public void Dispose() { Console.WriteLine($"Dispose - Begin"); Console.WriteLine($"Dispose - End"); } } ``` To see the effect of this new lifecycle method we need to navigate away from the component, this will remove it from the DOM and invoke the Dispose method. Blazor understands the `IDisposable` interface, when it detects its presence on a component it will call the Dispose method at the correct point when destroying the component instance. As of .NET 5, Blazor also supports the `IAsyncDisposable` interface. This allows disposal of resources asynchronously. This is useful when using JavaScript interop. But for now, note that `IDisposable` and `IAsyncDisposable` can’t both be implemented on the same component. ## Communicating between parent and child components A great analogy for components is Lego blocks. Each Lego block is a self-contained unit, but the real fun comes when you plug the blocks together in order to build something bigger and better. This is the same for components, they can be useful on their own, but they are more powerful when used together. In order to do this in any meaningful way, components need to be able to communicate with each other, passing data, and firing and handling events. In Blazor, we achieve this using *component parameters*. Component parameters are declared on a child component which forms that components API. A parent component can then pass data to the child using that API. But component parameters can also be used to define events on the child that the parent can handle. This allows data to be passed from the child back up to the parent. We’ll add a view button to the TrailCard which when clicked, will slide open a drawer on the right of the application. This drawer will display more detailed information about the selected trail. For this to work we will need to have three different components communicate and pass data. The HomePage component will coordinate the operation. It will handle any OnSelected events from the TrailCard component. When an OnSelected event is raised, the HomePage component will record the selected trail and pass it into the TrailDetails component. Inside the TrailDetails component, whenever the trail value changes, it will be the trigger for the drawer to active and slide into view. ### Passing values from a parent to a child In order to build our new drawer, we need to create a component which takes in a trail and then displays its information. We will use a component parameter to create its API. #### The TrailDetails component The `TrailDetails` component will display the selected trail which is passed in via a component parameter. ``` @if (Trail != null) { @Trail.Name @Trail.Location @Trail.Time @Trail.Length km @Trail.Description Close } @code { private bool _isOpen; [Parameter] public Trail? Trail { get; set; } protected override void OnParametersSet() { if (Trail != null) { _isOpen = true; } } } ``` #### Define a component parameter A component parameter is defined as a public property which is decorated with the `Parameter` attribute. Blazor uses this attribute to find component parameters during the execution of the `SetParametersAsync` lifecycle method we looked at earlier in the chapter. During this lifecycle method, the reflection sets the parameter values. We’re using the OnParametersSet lifecycle method to trigger the drawer sliding into view. As we learned earlier, this lifecycle method is run every time the components parameters change. This makes it perfect for our scenario as we can use it to trigger opening the drawer. #### Drawer implementation Opening and closing the drawer is done using CSS. When a new trail is passed in, the `isOpen` field is set to `true`, this triggers the logic at the top of the component to render the slide CSS class. ``` ``` In the `app.css` file (this is found in **wwwroot** > **css** folder) we need to add the styles to the bottom of the file. ``` .drawer-mask { visibility: hidden; position: fixed; overflow: hidden; top: 0; right: 0; left: 0; bottom: 0; z-index: 99; background-color: #000000; opacity: 0; transition: opacity 0.3s ease, visibility 0.3s ease; } .drawer-wrapper.slide > .drawer-mask { opacity: .5; visibility: visible; } .drawer { display: flex; flex-direction: column; position: fixed; top: 0; right: 0; bottom: 0; width: 35em; overflow-y: auto; overflow-x: hidden; background-color: white; border-left: 0.063em solid gray; z-index: 100; transform: translateX(110%); transition: transform 0.3s ease, width 0.3s ease; } .drawer-wrapper.slide > .drawer { transform: translateX(0); } .drawer-content { display: flex; flex: 1; flex-direction: column; } .trail-details { padding: 20px; } .drawer-controls { padding: 20px; background-color: #ffffff; } ``` The two key parts of the styles above are the `transform: translateX` properties on the `.drawer` and `.drawer-wrapper.slide > .drawer` classes. Without these properties, the drawer would sit in its open position, in full view. Figure 3.12 shows the effect of the properties on the drawer. The transform property on the `.drawer` class repositions the drawer off the right-hand side of the screen by 110% of its width. The transform property on the `.drawer-wrapper.slide > .drawer` class repositions it back to its default, bringing it into view. #### Updating the HomePage component To pass the trail into the *TrailDetails* component, we use attributes when defining the component in the parent. The parent for us is the *HomePage* component. Listing 3.10 shows the HomePage component updated with the TrailDetails component. ``` @page "/" @inject HttpClient Http @if (_trails == null) { Loading trails... } else { @foreach (var trail in _trails) { } } @code { private IEnumerable _trails; private Trail? _selectedTrail; protected override async Task OnInitializedAsync() { try { _trails = await Http.GetFromJsonAsync("trails/trail-data.json"); } catch (HttpRequestException ex) { Console.WriteLine($"There was a problem loading trail data: {ex.Message}"); } } } ``` In the HomePage component we have defined a field called `_selectedParameter` which will store the selected trail. We then pass this into the *TrailDetails* component using an attribute style syntax. ``` ``` The attribute name matches the component parameter we defined on the TrailDetails component. It is important that the case also matches otherwise Blazor will consider it a regular HTML attribute and ignore it. If you’re using an IDE such as Visual Studio for Windows or Mac, or JetBrains Rider you will receive IntelliSense to help you do this. Visual Studio Code also has IntelliSense support for Blazor via the C# extension. ### Passing data from a child to a parent We have successfully used a component parameter to define the API of the TrailDetails component, but we can’t see the fruit of our labor yet. In order to see something happen on screen we need to be able to select a trail to display. To do this we need to be able to pass that information up from the TrailCard component to the HomePage component. To do this we are going to use component parameters to define an event on the TrailCard. This event will pass the selected trail, the HomePage component can then handle this event and pass the trail to the TrailDetails component to display. ``` @Trail.Name @Trail.Location @Trail.Time @Trail.Length km View @code { [Parameter] public Trail Trail { get; set; } [Parameter] public Action OnSelected { get; set; } } ``` We define the event as a delegate of type `Action`. This allows us to pass the trail which this `TrailCard` is displaying back to the parent component. This happens when the View button is clicked. We handle the buttons click event using Blazor’s `@onclick` event. With the `TrailCard` updated all that’s left to do is handle the event in the `HomePage` component. First, we need to add a method to the code block which will be called whenever an event is raised. ``` private void HandleTrailSelected(Trail trail) { _selectedTrail = trail; StateHasChanged(); } ``` #### Code explained This method accepts the selected trail, assigns it to the `_selectedTrail` field. However, in order to see anything happen we must call `StateHasChanged` – this is to let Blazor know that we need the UI to update. The reason we must do this manually, is that Blazor can’t know the intent of our code. It has no idea that our custom event should trigger a re-render of the UI. There are some cases where this manual control over re-renders is preferred, however, in most cases this is just an extra line of code which must be added to achieve the desired effect. There is another way. We can use a different type to define our event on the `TrailCard` called `EventCallback`. By using this type for our event, Blazor will automatically call `StateHasChanged` on the component which handles the event, removing the need to manually call it. To take advantage of this we can update the component parameter on `TrailCard` and update how the event is invoked. ``` @Trail.Name @Trail.Location @Trail.Time @Trail.Length km View @code { [Parameter] public Trail Trail { get; set; } [Parameter] public EventCallback OnSelected { get; set; } } ``` Then, we can simply remove the `StateHasChanged` call from our handler in the `HomePage` component: ``` private void HandleTrailSelected(Trail trail) { _selectedTrail = trail; } ``` The final update is to assign the `HandleTrailSelected` method to the `OnSelected` event. We do this the same way we did to pass the selected trail into the TrailDetails component, using attributes. ``` ``` If all has gone to plan, then clicking the view button should trigger the drawer and display the trail. Clicking the close button at the bottom of the drawer will close it and allow a new trail to be selected. ## Styling components The styling is an important element to building any application and a powerful tool in delivering great UX. Look at the drawer we just built, the ability for it to slide in and out of the viewport was achieved using CSS, not C#. There are two approaches to styling component-based applications such as Blazor: - Global styling - Scoped styling As you would expect, global styles are classes which are declared on the global scope and can apply to any element which uses that class name or meets the selector for that class. Scoped styles are the opposite, a stylesheet is created for a specific component and any classes defined in it are made unique to that component using a unique identifier produced during the build process. No matter which of these approaches you take to style your application, it is possible to combine it with CSS preprocessors. CSS preprocessors like SASS, allow CSS to be written in a more modular and maintainable way – taking advantage of features such as variables and functions. ### Global styling Global styling is the default method when building applications. This is how we have been styling Blazing Trails up to this point. To apply global styling, one or more stylesheets are added to the host page which, by default, is *index.html* in Blazor WebAssembly and *\_Host.cshtml* in Blazor Server. The styles defined in those stylesheets are then available throughout the application. Global styles are fantastic for creating a consistent look and feel across an application. For example, if all buttons needed to be blue with certain font size and rounded corners. This can be defined once in a global style and would apply to all buttons in the app: ``` button { font-size: 1rem; background-color: blue; border-radius: .25rem } ``` This makes global styles an incredibly powerful tool because if we wanted to change how the buttons, or any aspect of the applications design looked, we can change the styles in one place, and the application is immediately updated. #### Why a global CSS? This global scope of styles can also cause some issues when developing larger applications. For example, if we wanted a certain button to be green with square corners rather than the global blue style above, we would need to add another style to the stylesheet. We would then need to apply the style to the particular button. That doesn’t seem too bad, but think of this happening many times over, you end up with a stylesheet which is full of one-off styles or niche styles. You could say this is down to bad design or lack of maintenance, which would be fair, but it still doesn’t stop it happening. Making changes to global styles can also be cumbersome. Constantly scrolling up and down a stylesheet with 100’s of lines of style classes can become tedious. Especially, when changes need to be made in multiple places. There are mitigations for this of course, using a CSS preprocessor like SCSS allows the global styles to be broken up and kept next to the component they are for in the project structure. This makes working with them much easier and more efficient. There is also another option which has come about with the rise of SPA frameworks, scoped CSS. ### Scoped styling Scoped styling works by allowing a developer to create styles which only effect a certain component in the application – this is done by creating a stylesheet with the same name as the component. During the build process, Blazor generates unique IDs for each component and then the styles for that component are rescoped using each ID. To get a feel for this, let’s rework the styles for the TrailDetails component we just built to use scoped CSS. To do this, we first need to create a new stylesheet called TrailDetails.razor.css, then take all the styles we added to app.css for the TrailDetails component and move them to this file. It is important that we name the file this way otherwise Blazor won’t pick it up and associate its styles with the `TrailDetails` component. If you’re using Visual Studio, a nice effect of this naming convention is the file nesting in Solution Explorer. #### Example of scoped styling When using scoped CSS, there will be a lot of stylesheets dotted around the application. Adding each and every one of them to the host page would be tedious and difficult to maintain. So, what Blazor does as part of the build process is bundle all the styles from the various stylesheets into a single stylesheet. This means we just need to reference that one stylesheet in our host page. The file has a naming convention of `[ProjectName].styles.css`. As our project is called BlazingTrails.Web, the file will be called, `BlazingTrails.Web.styles.css`. Listing 3.13 demonstrates where to reference the file. ``` BlazingTrails.Web ``` If we run the project and select a trail to open the drawer, we can use the browsers dev tools to look at the HTML and styles produced. ![Inspecting the HTML of the application in a browser shows a unique ID applied to each HTML element belonging to the TrailDetails component](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/03_img_0015.png?w=640&ssl=1)Inspecting the HTML of the application in a browser shows a unique ID applied to each HTML element belonging to the TrailDetails component Each HTML element belonging to the TrailDetails component now has a unique attribute applied to it. This attribute follows the format of `b-[uniqueID]`. If we then select an element to inspect its styles. Each selector for the styles in the TrailDetails.razor.css file has been rewritten to use the unique ID Blazor generated for the component. Doing this is what scopes the style to that component and stops the style effecting another element in another component. #### Global styles can still have an effect If you use scoped styles and nothing else in your application, then what I’m about to say isn’t an issue. However, if you have some global styles and some scoped styles then you may still run into issues. To give an example, let’s say we had the following CSS class called `.drawer` in our global CSS file, in addition to the one we have in the TrailDetails components scoped stylesheet: ``` .drawer { border: 5px solid lawngreen; } ``` As you can see, using scoped styles doesn’t make components immune from the standard behavior of CSS. This is something to think about when deciding on how to style your components, mixing global and scoped styles could make things more complicated. ### Using CSS preprocessors Whether you choose to use global styles, scoped styles, or a mix of both, you can still leverage the power of CSS preprocessors. Preprocessors work in a similar way as TypeScript does for JavaScript, as a superset language. They provide access to a richer feature set than CSS provides alone. There are many options out there when it comes to CSS preprocessors, but the main players are: - LESS () - SASS/SCSS () - Stylus () They all provide similar feature sets which offer the following enhancements over regular CSS, just with different syntaxes. - **Mixins** – reusable groups of styles - **Variables** – works the same way as variables in C# - **Nesting** – the ability to define the scoped of a style by writing it within another - **Import** – allows us to organize large CSS files into smaller more focused files. Then import common aspects such as variables. Choosing a preprocessor largely comes down to which syntax you prefer. My favorite preprocessor is SCSS (). It has a syntax very similar to regular CSS which makes everything easy to read. Also, it has been around for a very long time so there’s lots of documentation and blog posts out there to help if you get stuck. #### Integrating a CSS preprocessor I’m going to show you how to integrate SCSS into a Blazor app, specifically when using scoped CSS. There are two ways we can integrate SCSS into our application, using JavaScript tools or not using .NET tools. If you don’t want to use any JavaScript tools in your application, then I would suggest of the following options. - WebCompiler from Mads Kristensen (). This hasn’t had any meaningful updates for a couple of years, but it does still appear to work. - WebCompiler by excubo-ag (). This is forked from Mads WebCompiler and is looking like a promising project. It uses a dotnet CLI tool to perform the compilation of SCSS files and ties in with MSBuild. However, it currently only supports SCSS. Meaning if you are using a different preprocessor such as LESS or Stylus you are out of luck. Configuration is a bit difficult and there is limited documentation. The option I prefer, and I’m going to show you, is to use a mix of NPM and MSBuild. This does require having an up to date version of NodeJS installed which can be downloaded at . The version I’m using is 14.15.0 and is the latest LTS version. #### Integrate tools We’re going to use a tool called dart-sass () which we can install as an NPM package called sass (). We’re then going to use MSBuild to call this tool during the build process, specifically at the start of the build process. This is important as we need to compile our SCSS files to CSS before Blazor’s compiler runs so it can pick up the compiled CSS files and bundle them into the single \[ProjectName\].style.css file we talked about earlier. This new SCSS version of the `TrailDetails` styles only has one slight modification, it’s using the nesting feature from SCSS. It will allow us to confirm that the compilation steps worked and that the SCSS generates CSS. ## Conclusion Finally, we did it! We understand how working with Blazor’s component model and it works. If you have any problem, you have the source code on [GitHub](https://github.com/erossini/BlazorTrails). If you have any question, please use the [forum](https://forum). Happy coding! **Categories:** .NET, Blazor, C#, Visual Studio **Tags:** blazor, blazor-component, blazor-webassembly, components, viewcomponents --- ### [Setting up a Blazor WebAssembly application](https://puresourcecode.com/dotnet/net-core/setting-up-a-blazor-webassembly-application/) **Published:** April 22, 2021 **Author:** Enrico **Excerpt:** Setting up a Blazor WebAssembly application creates a new solution for a simple project to explore components and interactions with Blazor **Content:** Welcome to “Setting up a [Blazor](https://puresourcecode.com/?s=blazor) [WebAssembly](https://puresourcecode.com/?s=webassembly) application” post! In this new post I’ll build a simple project in Blazor and explain the basic Blazor components and interactions. The source code of the project I’m going to create in this post is available on [GitHub](https://github.com/erossini/BlazorTrails). Here the posts I wrote about Blazor that help you to learn this new technology better and faster: - [Getting Started With C# And Blazor](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/) - [Setting up a Blazor WebAssembly application](https://puresourcecode.com/dotnet/blazor/setting-up-a-blazor-webassembly-application) - [Working with Blazor’s component model](https://puresourcecode.com/dotnet/blazor/working-with-blazors-component-model/) Blazor is a framework for building interactive client-side web UI with [.NET](https://docs.microsoft.com/en-us/dotnet/standard/tour): - Create rich interactive UIs using [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) instead of [JavaScript](https://www.javascript.com/). - Share server-side and client-side app logic written in .NET. - Render the UI as HTML and CSS for wide browser support, including mobile browsers. - Integrate with modern hosting platforms, such as [Docker](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/container-docker-introduction/index). Using .NET for client-side web development offers the following advantages: - Write code in C# instead of JavaScript. - Leverage the existing .NET ecosystem of [.NET libraries](https://docs.microsoft.com/en-us/dotnet/standard/class-libraries). - Share app logic across server and client. - Benefit from .NET’s performance, reliability, and security. - Stay productive with [Visual Studio](https://visualstudio.microsoft.com/) on Windows, Linux, and macOS. - Build on a common set of languages, frameworks, and tools that are stable, feature-rich, and easy to use. So, here we’re going to start off by looking at the available templates provided by Microsoft to create a new application. Templates are a great way to get started quickly and provide all of the primary building blocks we need for a working an application. Once we have an understanding of the options, we’ll then choose a template as the base for our Blazor Trails app. We’ll build and run the template so we can get a feel for how it behaves, then we’ll strip out all of the unnecessary parts leaving us with only the key components. As a result, this is a screenshot of the web application I’m going to explain. ![Blazor Trails home page: final - Setting up a Blazor WebAssembly application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-7.png?resize=640%2C553&ssl=1)Blazor Trails home page: final ## Table of contents - [Setting up the application](#sigil_toc_id_11) - [Blazor WebAssembly template configurations](#sigil_toc_id_12) - [Standalone mode](#h-standalone-mode) - [Hosted mode](#h-hosted-mode) - [Creating the application](#sigil_toc_id_13) - [Building and running the application](#sigil_toc_id_14) - [Local SSL certificate](#h-local-ssl-certificate) - [Key components of a Blazor application](#sigil_toc_id_15) - [Index.html](#sigil_toc_id_16) - [More details about Index.html](#h-more-details-about-index-html) - [Base tag](#h-base-tag) - [Program.cs](#sigil_toc_id_17) - [More details about Program.cs](#h-more-details-about-program-cs) - [Dependency injection](#h-dependency-injection) - [App.razor](#sigil_toc_id_18) - [Router component](#h-router-component) - [wwwroot folder & \_imports.razor](#sigil_toc_id_19) - [Writing your first components](#sigil_toc_id_20) - [Organizing files using feature folders](#sigil_toc_id_21) - [Routable component](#h-routable-component) - [Defining the layout](#sigil_toc_id_22) - [Main layout](#h-main-layout) - [The Blazor Trails home page](#sigil_toc_id_23) - [Prepare the data](#h-prepare-the-data) - [The first injection](#h-the-first-injection) - [Read the data](#h-read-the-data) - [JsonAsync methods](#h-jsonasync-methods) - [Waiting for the data](#h-waiting-for-the-data) - [Refactor](#h-refactor) - [Conclusion](#h-conclusion) ## Setting up the application So, in other frameworks setting up a new application involves creating everything manually, from scratch. Generally speaking, .NET applications aren’t created this way. Many, if not all, start life being generated from a template. Using a template has many advantages: - Developers can have a working application in seconds - Boilerplate code is taken care of and doesn’t need to be written for every new application - The template serves as a working example of using the framework - The process is repeatable, using a template will give you the same starting point time and time again Then, Blazor comes with two templates which can be used to create new applications. When choosing a template, we’re essentially making the choice of which hosting model we want to use, either **Blazor Server** or **Blazor WebAssembly**. In fact, the two available templates are named *Blazor Server* and *Blazor WebAssembly* which makes knowing the hosting model they use pretty straightforward. However, we’re going to be using *Blazor WebAssembly* to build our Blazor Trails application so that is the template type we’re going to focus on in this post. ### Blazor WebAssembly template configurations So, before we actually create the application, I want to talk about the configuration options available for the Blazor WebAssembly template. This template is the more complex of the two available because you can configure it in two modes: *hosted* or *standalone*. ![The left side shows the projects created when configuring the template in hosted mode. The right shows the project created when configuring the template in standalone mode - Setting up a Blazor WebAssembly application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/02_img_0002.png?w=640&ssl=1)The left side shows the projects created when configuring the template in hosted mode. The right shows the project created when configuring the template in standalone mode. #### Standalone mode In the standalone mode, which is the default configuration, you will end up with a single Blazor WebAssembly project in the solution. This template is great if you’re looking to build an application which doesn’t need any kind of backend or server element to it, or perhaps you already have an existing API. #### Hosted mode Hosted mode is a little bit more complex. If you enable the ASP.NET Core Hosted option when creating the template, you will end up with three projects in the solution: - Blazor WebAssembly project - ASP.NET Core WebAPI project - .NET Standard class library In this configuration you are getting a full stack .NET application. A fully functioning backend (ASP.NET Core WebAPI), a place to put code which is shared between the frontend and backend project (.NET Standard class library), and the frontend application (Blazor WebAssembly). So, I want to highlight that using this configuration **does require a .NET runtime** to be installed on the host server. You may remember in my [previous post](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/) I mentioned an advantage of using Blazor WebAssembly was it didn’t require a .NET runtime on the server, that benefit doesn’t apply when you’re using the hosted configuration. This is because there is a full ASP.NET Core WebAPI project in the solution which does need a .NET runtime on the server to function. ### Creating the application So, I want to create a *Standalone mode* application. There are two way to create a new application using a template, the dotnet CLI (Command Line Interface) or via an IDE (Integrated Development Environment) such as Visual Studio. To create the application, open Visual Studio and follow these steps (there may be slight differences in wording or order of screen on other IDEs): 1. File > New Project. 2. From the project templates list select Blazor WebAssembly App. 3. The next screen allows us to set the name of the project and the solution as well as where the files will be saved on disc. Enter the details and then click *Next* to move to the next step. 4. Select .NET5 as **Target Framework** ![Create a new Blazor WebAssebly app - Setting up a Blazor WebAssembly application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-8.png?resize=640%2C425&ssl=1)Create a new Blazor WebAssebly app ![Configure your new Blazor project - Setting up a Blazor WebAssembly application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-9.png?resize=640%2C425&ssl=1)Configure your new Blazor project ![Additional information - Setting up a Blazor WebAssembly application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-10.png?resize=640%2C425&ssl=1)Additional information Be sure, **ASP.NET Core hosted** and **Progressive Web Application** are not checked. This will create a new application with the same configuration and folder structure we setup using Visual Studio. At this point you’ve created your first Blazor application, congratulations! Now we have our shiny new application we’re going to look at how we can build and run it. ### Building and running the application When it comes to running .NET applications there are 3 steps that need to happen: 1. Restore any packages (also referred to as dependencies) 2. Compile or build the application 3. Fire up a web server and serve the application In previous versions of .NET Core these steps needed to be performed manually so you would need to first restore any packages, then build the code, and finally run the app. However, this is no longer the case, we can now jump straight to running the application and either Visual Studio or the .NET CLI will take care of performing the first two steps, if they’re required. However, it’s always good to understand how to perform these steps yourself manually if the need arises. When using Visual Studio, you can restore packages by right clicking on the solution and selecting Restore NuGet Packages from the context menu. If you’re using the .NET CLI then you can execute the `dotnet restore` command. To perform a build from Visual Studio, select `Build` > `Build Solution` from the top menu. You can also use a keyboard shortcut to perform the same task, **Ctrl+Shift+B**. From the .NET CLI you can use the `dotnet build` command. Performing a build will also perform a package restore, if it’s required, both when using Visual Studio or the CLI. So, having to manually restore packages shouldn’t be an issue. All that’s left is to run the application. From Visual Studio this can be done in several ways. First, you can press the play button found in the main toolbar. You can also use a keyboard shortcut which is **F5**. Finally, you can select `Debug` > `Start Debugging` from the top menu. Any of the above will run the application and Visual Studio will fire up a browser and load the application automatically. #### Local SSL certificate Depending on what types of application you’ve created and run before, you could see an extra step which asks if you want to trust the development SSL certificate. Answering yes to this will install the development certificate on your machine and this allows the application to be run over https rather than http. I would recommend trusting and installing the development SSL certificate as running sites over https is best practice, even in development as it mimics the live environment. ![First run of a boilerplate Blazor app - Setting up a Blazor WebAssembly application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-11.png?resize=640%2C333&ssl=1)First run of a boilerplate Blazor app ## Key components of a Blazor application While the template has generated a fair few files, some of these files are more important to know and understand than others. In this section, we’re going to look at each of those key files to understand what they do and why they’re important. Then we’re going to remove all of the other files from our project to give us a clean base ready to start building Blazor Trails. ### Index.html This is one of the most important components of a Blazor WebAssembly application. It can be found in the `wwwroot` directory of the project and it’s the host page for the Blazor application. ``` BlazorTrailsWA Loading... An unhandled error has occurred. Reload 🗙 ``` - 8: the tag base is used by Blazors router to understand which routes it should handle - 15: the app tag is where the Blazor application will load - 19: this div is displayed automatically by Blazor when an unhandled exception occurs - 22: Blazors JavaScript runtime which downloads and initializes the application #### More details about Index.html The key element in the `index.html` file is the link to the **Blazor JavaScript runtime**, found near the bottom of the page. As we saw [previously](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/), this is the file which downloads the .NET WebAssembly based runtime as well as the application and any of its dependencies. Once this is complete it also initializes the runtime which loads and runs the application. When the application runs its content needs to be outputted somewhere on the page and, by default, this is outputted to the app tag. This is configurable and is setup in the `Program.cs` file which we will look at in a second. Any default content which exists in the tag will be replaced at runtime with the output from the application. This has a useful benefit; initial content can be used as a placeholder which will be displayed to the user until the application is ready. If there is ever an unhandled exception caused inside the application, then Blazor will display a special UI which signals to the user that something has gone wrong. This is defined here in the `index.html`. This can be customized however you would like but the containing element much have an `id` attribute with the value `blazor-error-ui`. The default message states there has been a problem and offers the user a button which will cause a full page reload. This is the only safe option at this point as the application will be in an *unknown* state. ![Exception bar in a Blazor application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-12.png?resize=640%2C365&ssl=1)Exception bar in a Blazor application #### Base tag The final key piece to the `index.html` file is the `base` tag. This is an important tag when it comes to client-side routing. The reason this tag is important is that is tells Blazors router what URLs, or **routes**, are in scope for it to handle. If this tag is missing or configured incorrectly then you may see some unexpected or unpredictable behavior when navigating your application. By default, the tag is configured with a value of `/`. This means that the application is running at the root of the domain (for example [www.puresourcecode.com](https://puresourcecode.com/)). The router should handle all navigation requests within that domain. However, if the application was running as a sub-application for example `https://puresourcecode.com/blazortrails`, then the base tag would need to reflect this with a value of `/blazortrails/`. This would mean the router will only handle navigation requests which start with `/blazortrails/`. ### Program.cs Just like other ASP.NET Core applications, Blazor apps start off as .NET console apps. What makes them a Blazor application is the type of host they run. In the case of Blazor WebAssembly it runs a `WebAssemblyHost`. The purpose of the code contained in this file is to configure and create that host, figure 2.9 shows the default configuration of the `Program` class. ``` public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync(); } } ``` - 17: create an instance of `WebAsseblyHostBlazor` - 18: define the root component for the application - 20: configure and register services with the `IServiceCollection` - 24: build and run an instance of `WebAssemblyHost` using the configuration defined with the `WebAssemblyHostBuilder` #### More details about Program.cs There are two critical pieces of configuration happening, the root component for the application is defined and any services are configured and added to the `IServiceCollection`. When defining the root components – there can be more than one although that is usually not the case in most applications – we are actually giving the builder two pieces of information. The first is the type of the root component for the application. By default, this is the `App` component (which we will look at next). However, you can configure this to be any component you wish. The second is the place in the host page where we want to inject the application. The standard setup has the application being injected into the app element we looked at previously on the `index.html` page. But again, you can configure this to be injected anywhere you wish. The argument the `builder.RootComponents.Add` method takes is a CSS selector which is used to identify the target element where the component will be injected. Specific elements can be targeted such as `app` or elements with a specific ID, for example, `#root-component`, or any other valid [CSS selector](https://www.w3schools.com/cssref/css_selectors.asp). #### Dependency injection The next line shows the `HttpClient` being configured and registered with the `IServiceCollection` making it available to classes and components via [dependency injection](https://puresourcecode.com/dotnet/net-core/architecting-asp-net-core-applications#h-dependency-inversion-di) (DI). Blazor uses the same DI container as other ASP.NET Core apps and allows registering of services using one of 3 lifetimes: 1. **Transient** – A new instance is provided each time it’s requested from the service container. Given a single request, if two objects needed an instance of a transient service, they would each receive a different instance. 2. **Scoped** – A new instance is created once per request. Within a request you will always get the same instance of the service across the application. 3. **Singleton** – An instance is created the first time it’s requested from the service container, or when the `Program.Main` method is run, and an instance is specified with the registration. The same instance is used to fulfil every request for the lifetime of the application. The last thing that the Main method does is to take all of the configuration specified with the `WebAssemblyHostBuilder` and call its `Build` method. This will create an instance of a `WebAssemblyHost` which is the heart of your Blazor app. It contains all of the application configuration and services needed to run your app. ### App.razor This is the root component for a Blazor application, and we saw how this was configured in the `Program.Main` method in the previous section. This doesn’t have to be the case however; you can configure a different component to be the root component if you wish, or even have multiple root components, you just need to update the configuration in the `Program.Main` method. #### Router component The App component contains a vital component for building multi-page applications, the `Router` component. The Router component is responsible for managing all aspects of client-side routing. When an application first starts up, the router will use reflection to scan the applications assemblies for any routable components. Then, it stores information about them in a routing table and whenever a link is click or navigation is triggered programmatically, the router will look at the route which has been requested and try and find a routable component which handles that route. If a match is found, then it will load that component, otherwise it will load a not found template which is specified inside the Router component. ### wwwroot folder & \_imports.razor Now, I’m going to cover both of these files in this section as there is not a huge amount to say about them. In fact the `_imports.razor` file is the one component on this list which is not required to run a Blazor application, but it makes things a lot easier if you do use it. By convention all ASP.NET Core applications have a `wwwroot` folder which is used to **store public static assets**. This is the place where you can put things such as images, CSS files, JavaScript files or any other static files you need. Anything you put in this folder will be published with your application and available at runtime. As I mentioned earlier, this is also where the `index.html` file is kept. The `_imports.razor` file is optional and not required when building a Blazor application, however, it’s really useful to have at least one of these files. The `_imports.razor` file has a simple job, it contains using statements. What is really great about the way this file works is that it makes those using statements available to all of the components in that directory and any sub-directories. This saves you having to add common using statements to every component in your application. As I alluded to, you can also have multiple version of this file at different points in your folder structure. So, if you had a structure of `BlazorTrails > Features > Home`, and you only wanted specific using statements to be applied to components in the *Home* folder. Then you could add a `_Imports.razor` file in the *Home* folder with those using statements and they would only apply there but would still inherit any using statements from `_Imports.razor` files higher in the structure. ## Writing your first components We’ve had a look at the app created by the template, and we’ve covered each of the key files and, at a high level, what they do. Now it’s time to write some code of our own. As I said at the start of the chapter, we’re going to be building the foundations of the Blazor Trails application. ![List of trails](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/image-14.png?resize=640%2C516&ssl=1)List of trailsThe first thing we’ll do is take about how the application files are going to be organized. Next, we’ll remove all of the unneeded files which were generated by the template. This will give us a clean base to start building from. We’ll then define several new components to create what you see, a layout component, a page component and a couple of regular components. ### Organizing files using feature folders Before we start adding our own code, we need to remove all of the unnecessary files generated by the template. By default, the app structure used by the template divides files by responsibility. There’s a *Pages* folder for routable components. There is a *Shared* folder for anything which is used in multiple places or is a global concern. This kind of separation doesn’t scale well and makes adding or changing functionality much more difficult as files end up being spread out all over the place. Instead we’re going to use a system called *feature folders* to organize our application. When using feature folders all of the files relating to that feature are all stored in the same place. The has two major benefits, first, when you go to work on a particular feature all of the files you need are in the same place making everything easier to understand and more discoverable. The second is that it will scale well, every time you add a new feature to the app you just add a new folder, and everything goes in there. You can also arrange each feature with sub-features if they contain a lot of files. ### Routable component The other little thing I like to do when using this organization system with Blazor is to append any routable component with the word *Page*. The reason is when a feature has several components in it it’s almost impossible, at a glance, to see which one is the routable component. The only real way to know is to open the file and check for the `@page` directive at the top. So, start by deleting the Pages and Shared folders along with their contents, then delete the sample-data folder from the wwwroot folder. Also delete most of the contents of the app.css, just leave the import statement for the open iconic styles and the class called `#blazor-error-ui` and `#blazor-error-ui .dismiss`. We also need to delete the last using statement from the `_Imports.razor` file, `@using BlazorTrailsWA.Shared`. Add a new folder at the root of the project called *Features*, then inside that add a folder called *Layout* and another called *Home*. Inside Layout, add a new Razor Component called `MainLayout.razor`. Inside Home add a new Razor Component called `HomePage.razor`. Once you’ve done that head back over to the `_Imports.razor` and add the following using statements. ``` @using BlazingTrails.Web.Features.Home @using BlazingTrails.Web.Features.Layout ``` ### Defining the layout Blazor borrows the concept of a layout from other parts of ASP.NET Core and essentially it allows us to define common UI which is required by multiple pages. Things such as the header, footer and navigation menu are all examples of things you might put in your layout. We also add a reference to a parameter called `Body` where we want page content to be rendered. This comes from a special base class which all layouts in Blazor must inherit from called `LayoutComponentBase`. The following image shows an example of what might be defined in a layout along with where the rendered page content would be displayed. ![An example layout defining shared UI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/02_img_0014.png?w=640&ssl=1)An example layout defining shared UI You don’t have to stick with a single layout for your whole application either, you can have multiple layouts for different parts of your app. So, if you wanted a particular layout for the public facing pages but a different one for the admin pages, you can do that. In Blazor the default layout is defined on the Router component. This will automatically be applied to all pages in the application. ``` Sorry, there's nothing at this address. ``` In line 3 and 6 you see the default layout is defined by passing the type of the component you wish to use. If you want to use a different layout on certain pages, you can specify an alternative by applying the `@layout` directive. This goes at the top of the page and you pass the type of the component you wish to use. For example, if we had an alternative layout called *AdminLayout*, our layout directive would look like this: `@layout AdminLayout`. ### Main layout We’re going to be updating the MainLayout component. To start with we are going to do two things; First, we’re going to use the `@inherits` directive to inherit from the `LayoutComponentBase` class. This will mark this component as a layout component and will give us access to the `Body` parameter. Second, we’re going to define where our page content is rendered using the `Body` parameter. ``` @inherits LayoutComponentBase @Body ``` The only thing we’re missing from our layout now is the header. We’re going to define this as a separate component and as it’s part of the overall Layout feature it will go in the Layout feature folder next to the `MainLayout` component. As we did before, add a new Razor Component called `Header.razor`. Then, we’re going to add the markup shown which adds a Bootstrap navbar displaying the text. ``` Blazing Trails ``` That’s all we need in the Header component at the moment; we can now add that to the MainLayout by declaring it as we would any normal HTML element. ``` @inherits LayoutComponentBase @Body ``` That’s it for the layout, if you try and run the application at this point you will be able to see the header we’ve just created but there will be a message saying *“Sorry, there’s nothing at this address”*. That’s because we haven’t defined any routable components (pages) yet. ## The Blazor Trails home page We already created the `HomePage` component. It still has the boilerplate code which comes with a new component. We need to update this code to make the component routable. Once we have that done, we’re going to define a class which represents a trail. We can then define some test data to use to build out the rest of the UI. Finally, we’re going to load the test data into the `HomePage` and loop over it to display the various trails via a reusable `TrailCard` component that we’ll create. As we talked about earlier, to make a component into a routable component we need to use the `@page` directive and a route template which specifies the route it will be responsible for. At the top of the HomePage.razor file, add the directive along with a route template of `“/”`, which tells the Router that this page is the root page of the application. You can run the application at this point, if you wish, to check that the HomePage’s content is being displayed. We need a way of representing a trail in our code, to do that we’re going to add a new class called `Trail` to the *Home* feature folder. Inside this class we need to add a few properties which represent the various data about a trail. ``` public class Trail { public string Name { get; set; } public string Image { get; set; } public string Location { get; set; } public string Time { get; set; } public int Length { get; set; } } ``` ### Prepare the data Now, we have a definition for a trail we’re going to define some test data to use. At the moment our app doesn’t have a backend, there is no API we can call to retrieve or save data from and to, but later on there might be. In order to simulate making an HTTP call to load data from an API we’re going to define our test data in a `json` file. So, this is a great way to develop frontend applications which don’t currently have a useable server element. We can still use a `HttpClient` to load the data from the JSON file in the same way we’d load data from an API. Then once the server element is established, the HTTP call just needs to be updated to point at the API endpoint instead of the JSON file. Now, in the `wwwroot` folder create a directory called *trails*. Inside that folder add a new json file called *trail-data.json*. You have the full json on [GitHub](https://github.com/erossini/BlazorTrails). Then, with our test data in place we’ll return to the `HomePage` where we need to load it. We’re going to load the data using the `HttpClient`, but in order to use it we need to get an instance of it using dependency injection. Blazor makes this really easy by providing an inject directive that has the following format, `@inject [TYPE] [NAME]`, where \[Type\] is the type of the object we want and \[Name\] is the name we’ll use to work with that instance in our component. ### The first injection So, under the page directive add the following code which will give us an instance of the `HttpClient` to work with: `@inject HttpClient Http`. Before we can use the `HttpClient`, we need somewhere to store the results the call will return. Our JSON tests data is an array of trails and as we’re not going to be modifying what’s returned, just listing it out, we can create a private field of type `IEnumerable`. ``` @page "/" @inject HttpClient Http HomePage @code { private IEnumerable _trails; } ``` ### Read the data Now, we have somewhere to store our test data we can make the call to retrieve it. A great place to do this kind of thing is the `OnInitialized` life-cycle method. This method is provided by `ComponentBase`, which all Blazor components inherit from, and it one of three primary lifecycle methods; The other two are `OnParametersSet` and `OnAfterRender`, they all have async versions as well. `OnInitialized` is only run once in the component’s lifetime making it perfect for loading initial data like we need to. In order to retrieve the data from the JSON file, we can make a GET request just like we would if we were reaching out to an API. Except, instead of passing the address of the API in the call, we pass the relative location of the JSON file. As the file is in the wwwroot folder it will be available as a static asset at runtime just like the CSS file, this means the path we need to pass in the GET request is simply, `“trails/trail-data.json”`. ### JsonAsync methods So, a great productivity enhancement which ships with Blazor is the addition of some extension methods for the `HttpClient`: - GetFromJsonAsync - PostAsJsonAsync - PutAsJsonAsync Under the hood, these methods are using the new `System.Text.Json` library. The first method will deserialize a successful response containing a JSON payload to a type we specify. The second and third will serialize an object to JSON to be sent to the server. All three of these methods do this in a single line. No more having to manually serialized and deserialize objects or check for success codes, making everything much cleaner and removing a lot of boilerplate. Also, one thing to be aware of when using these new methods is that when a non-success code is returned from the server, they’ll throw an exception of type `HttpRequestException`. This means that it’s generally a good practice to wrap these calls in a try catch statement so non-success codes can be handled gracefully. ``` @code { private IEnumerable _trails; protected override async Task OnInitializedAsync() { try { _trails = await Http.GetFromJsonAsync("trails/trail-data.json"); } catch (HttpRequestException ex). { Console.WriteLine($"There was a problem loading trail data: {ex.Message}"); } } } ``` Great! We now have our data being loaded into our component, but we need to do something with it. It would be nice to have a message displayed to the user to let them know that when we’re loading the data, just in case it takes a while. ### Waiting for the data We can use a simple if statement in our markup to check the value of the `_trails` field. If it’s null then we can surmise that the data is still being loaded, excluding an error scenario of course. If the value is not null, then we have some data and we can go ahead and display it. ``` @if (_trails == null) { Loading trails... } else { @foreach (var trail in _trails) { @trail.Name @trail.Location @trail.Time @trail.Length km } } ``` At the point you should be able to build the app and run it, if all has gone to plan you should see the trails displayed. Now, we could finish here but there’s one little refactor I think we should do first. ### Refactor While it’s all perfectly valid as is, wouldn’t it be nice to encapsulate it all in a component instead? This would make the code in the `HomePage` component much easier to read. So, create a new component called `TrailCard.razor` in the Home feature folder. Then replace the boilerplate code with the markup for the card from the `HomePage`, be careful not to copy the outer div with the class’s `col mb-4`. That was pretty painless, but now we have a problem. How do we get access to the current trail data? The answer is parameters. Now, we can pass data into components via *parameters*, you can think of these as the public API for a component and they work one way, from parent to child. We can define them in the code block by creating a public property and decorating it with the `Parameter` attribute. Then, we pass data into them from the parent using attributes on the component tag. For our `TrailCard` component we’re going to create a parameter which will allow us to pass in the current trail from the parent. We can then update the razor code to use this parameter. ``` @Trail.Name @Trail.Location @Trail.Time @Trail.Length km @code { [Parameter] public Trail Trail { get; set; } } ``` All that’s left now is to update the `HomePage` component to use the new `TrailCard` component. ``` @page "/" @inject HttpClient Http @if (_trails == null) { Loading trails... } else { @foreach (var trail in _trails) { } } @code { private IEnumerable _trails; protected override async Task OnInitializedAsync() { try { _trails = await Http.GetFromJsonAsync("trails/trail-data.json"); } catch (HttpRequestException ex) { Console.WriteLine($"There was a problem loading trail data: {ex.Message}"); } } } ``` ## Conclusion Finally, we did it! We finished setting up a Blazor WebAssembly application and it works. If you have any problem, you have the source code on [GitHub](https://github.com/erossini/BlazorTrails). If you have any question, please use the [forum](https://forum). Happy coding! **Categories:** .NET, .NET Core, Blazor, C#, Visual Studio **Tags:** blazor, blazor-webassembly, components, viewcomponents --- ### [Getting started with C# and Blazor](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/) **Published:** April 15, 2021 **Author:** Enrico **Excerpt:** Getting started with C# and Blazor explains how this new Microsoft technology is working and the basic information to understand Blazor **Content:** In this new post, I want to summarize what I understood for getting started with C# and [Blazor](https://puresourcecode.com/?s=blazor), the new technology from Microsoft. I briefly spoke about Blazor in [some other posts](https://puresourcecode.com/dotnet/net-core/how-adding-an-ui-built-in-blazor/) but here I want to introduce it properly. We live in exciting times, as .NET developer’s life has never been better. We can create apps for any operating system be it Windows, Linux, iOS, Android or macOS. Of course, we can also build amazing web-based applications with ASP.NET. MVC, Razor Pages, and WebAPI have allowed us to create robust scalable and reliable systems for years, but there has long been a missing piece to the puzzle. One thing all of ASP.NETs web solutions have in common is that they are server based. We’ve never been able to leverage the power of C# and .NET to write client-side applications, this has always been the domain of [JavaScript](https://puresourcecode.com/?s=javascript). So, I’m going to introduce you to a revolutionary client-side framework: [Blazor](https://puresourcecode.com/?s=blazor). Built on web standards, Blazor allows us to write rich, engaging user interfaces using C# and [.NET](https://puresourcecode.com/category/dotnet/). We’ll explore how Blazor can make your development process more efficient and raise your productivity levels, especially if you’re using .NET on the server as well. We’ll cover hosting models, an important concept to understand when starting out with Blazor. We’ll look at both production supported models and the benefits and tradeoffs of each. Next, we’ll introduction components and the benefits of using them to build UIs. Finally, we’ll discuss the reasons why you should consider Blazor for your next project. ## Table of contents - [Why choose Blazor for new applications?](#sigil_toc_id_1) - [Pros](#h-pros) - [Components, a better way to build UI.](#sigil_toc_id_2) - [What is a component?](#sigil_toc_id_3) - [The benefits of a component-based UI](#sigil_toc_id_4) - [Components](#h-components) - [Anatomy of a Blazor component](#sigil_toc_id_5) - [Understanding the code](#h-understanding-the-code) - [Blazor, a platform for building modern UI with C#](#sigil_toc_id_6) - [No installation required](#h-no-installation-required) - [Mobile applications](#h-mobile-applications) - [Understanding hosting models](#sigil_toc_id_7) - [Blazor Electron](#h-blazor-electron) - [Code example](#h-code-example) - [Mobile Blazor Bindings](#h-mobile-blazor-bindings) - [Blazor WebAssembly](#sigil_toc_id_8) - [Process begin](#h-process-begin) - [DOM manipulation](#h-dom-manipulation) - [blazor.boot.json](#h-blazor-boot-json) - [dotnet.wasm](#h-dotnet-wasm) - [Calculating UI Updates](#heading_id_3) - [Process explained](#h-process-explained) - [Benefits](#heading_id_4) - [Tradeoffs](#h-tradeoffs) - [Blazor WebAssembly summarize](#h-blazor-webassembly-summarize) - [Blazor Server](#sigil_toc_id_9) - [Process begins](#h-process-begins) - [Process static files](#h-process-static-files) - [Calculating UI updates](#heading_id_5) - [Process explained](#h-process-explained-1) - [SignalR](#h-signalr) - [DOM](#h-dom) - [Performance](#heading_id_6) - [The test](#h-the-test) - [Testing](#h-testing) - [Benefits](#heading_id_7) - [Tradeoffs](#h-tradeoffs-1) - [Blazor Server summarize](#h-blazor-server-summarize) ## Why choose Blazor for new applications? Arguably, the hardest part of starting a new project in recent times has been choosing the tech stack, there is just so much choice available. This is especially true in the front-end world. Pick a framework (Angular, React, Vue), pick a language (TypeScript, CoffeeScript, Dart), pick a build tool (Webpack, Parcel, Browserify). If a team is new to this eco-system, it can seem an almost impossible task to try and work out which combination of technologies will help make the project a success; it’s even hard for teams with experience! So, first in this getting started with C# and Blazor, let’s cover some of the top reasons for choosing Blazor for your next project and how it can help avoid some of the issues I’ve just mentioned. ### Pros - [**C#**](https://puresourcecode.com/category/dotnet/csharp/), a modern and feature rich language – It’s powerful, easy to learn, and versatile - **Great tooling** – The .NET community has been fortunate to have some amazing tooling. [Visual Studio](https://puresourcecode.com/category/dotnet/visual-studio/) is an extremely powerful, feature rich and extensible IDE. It’s also 100% free for individuals or non-enterprise teams of 5 or less. If you prefer something more lightweight, then there is Visual Studio Code – one of the most popular code editors today. Both Visual Studio and VS Code are both cross platform: - Visual Studio for Windows and Mac - Visual Studio Code for Windows, Mac and Linux. - **.NET Ecosystem** – While many new frameworks need to wait for an ecosystem to build up around them, Blazor can tap into the existing .NET ecosystem. Blazor applications target .NET Standard 2.1 and can in theory use any .NET Standard NuGet package. - **Unopinionated** – There are no preferred patterns or practices for Blazor development, you can write applications using the ones you’re familiar and comfortable with. - **Shallow learning curve** – If you’re an existing .NET developer then the learning curve for Blazor is quite shallow. Razor, C#, dependency injection, project structure will all look familiar to you. This means you can focus on writing features quicker, rather than learning the framework. - **Code sharing** – If you’re using C# on the server then Blazor makes an excellent paring. One of the most frustrating problems with different client and server languages is the inability to reuse code. With Blazor, everything is C#. Any shared code can be placed in a common .NET Standard class library and shared easily between server and client. - **Open source** – As with many projects at Microsoft, Blazor is fully open source and the code is freely available on GitHub for you to browse, download, or fork your own copy. ## Components, a better way to build UI. [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor), as with many modern front-end frameworks, uses the concept of components to build the UI. Everything is a component, pages, parts of a page, layouts, they’re all components. There are various types of component in Blazor as well as multiple ways to write them all of which will be explored in future chapters. But learning to think in terms of components is essential for writing Blazor applications. ### What is a component? You can think of a component as a building block. You put these building blocks together to form your application. These building blocks can be as big or as small as you decide, however, building an entire UI as a single component wouldn’t be a good idea. Components really show their benefit when you think of them as a way to divide up logical areas of a UI. Let’s look at an example of a user interface structured as components. ![Example of a layout divided into components - Getting started with C# and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0001.png?w=640&ssl=1)Example of a layout divided into components Each area of the interface is a component and each one has a certain responsibility. You may also notice that there is a hierarchy forming. The layout component sits at the top of the tree, the menu, header, home page and footer are all child components of the layout component. These child components could, and probably would have child components of their own. For example, the header component could contain a logo component and a search component. ![Example of nesting components to form a component tree - Getting started with C# and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0002.png?w=640&ssl=1)Example of nesting components to form a component tree ### The benefits of a component-based UI Many UIs have repeating elements in them, a great advantage to using components is that you can define an element in a component and then reuse the component wherever the element repeats. This can drastically cut down on the amount of repeated code in an application. It also makes the maintainability of the application much better as if the design of that element changes you only need to update it in a single place. To cater for more advanced scenarios, components can define their own APIs allowing data and events to be passed in and out. Imagine a line of business application, it’s probably safe to assume that within that app there would be lots of places data would be displayed in table format. One approach would be to create each table as its own component, however, this would mean we would end up with a lot of components which displayed data in a table. A better approach would be to define a single component which took in a dataset as a *parameter* and then displayed it in a table. Now we have a single component for displaying data in a table that we can reuse all over the application. We could also add features to this component, things such as sorting or paging. As we do, this functionality is automatically available to all the tables in the application as they are all reusing the same component. ### Components Components help speed up the development process. Due to the reusable nature of components, using them often leads to shorter development times. They can be composed together. While usually self-contained, it’s also possible to have components work together to create more complex UI. For example, let’s take the data table scenario we just talked about, that could be a single component but that could potentially be quite large. Another approach would be to divide it up into several smaller components, each performing a certain job. We could have a table header component, a table body component even a table cell component. Each of these components are performing a specific job but they are still part of the overall table component. ### Anatomy of a Blazor component Now, in this post getting started with C# and Blazor, we have a better idea of what components are in a general sense, let’s look at an example of a component in Blazor. For this we’re going to grab a component from the Blazor project template. In figure 1.3 we can see an example of a component from Blazors standard project template, *Counter.razor*. ![The sections of a component in Blazor - Getting started with C# and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0003.png?w=640&ssl=1)The sections of a component in Blazor This particular component is known as a *routable component,* as it has a *page directive* declared at the top. Routable components are essentially a page in the application. When the user navigates to the */counter* route in the application, this component will be loaded by Blazor router. It displays a simple counter with a button and when the user clicks on the button the count is incremented by one and the new value displayed to the user. #### Understanding the code While understanding the code isn’t important at this point, we can understand the structure of the component. Figure 1.3 is divided up into three sections each has a certain responsibility. - **Section 1** is used to define directives, add using statements, inject dependencies, or other general configuration which applies to the whole component. - **Section 2** defines the markup of the component; this is written using the Razor language, a mix of C# and HTML. Here we define the visual elements which make up the component. - **Section 3** is the code block. This is used to define the logic of the component. It is possible to write any valid C# code into this section. You can define fields, properties, even entire classes if you wish. ## Blazor, a platform for building modern UI with C# Blazor is a fully featured framework for building modern client-side applications using the power of C# and .NET. Allowing developers to build engaging applications which work across nearly any platform – including web, mobile and desktop. Blazor is an alternative to JavaScript frameworks and libraries such as Angular, Vue and React. If you’ve had experience working with any of these then you’ll probably start spotting familiar concepts. The most notable influence is the idea of building UIs with components, a concept all these technologies share and something we’ll explore in more detail later in this chapter. ### No installation required Because Blazor is built on top of web standards; it doesn’t require the end user to have .NET installed on their machines or any kind of browser plugin or extension. In fact, with Blazor WebAssembly applications we don’t even need .NET running on the server, this flavor of Blazor can be hosted as simple static files. Being built on .NET means we have access to the vibrant ecosystem of packages available on NuGet. We also have best in class tooling with Visual Studio and Visual Studio Code, and of course, with .NET being cross platform, we can develop our Blazor applications on whatever our preferred platform is, be that Windows, Mac or Linux. ### Mobile applications Therefore, I want to highlight that Blazors programming model can also be used to build cross-platform native mobile applications via an experimental project called Mobile Blazor Bindings. This is a collaboration between the ASP.NET Core team and the Xamarin team to investigate the potential and demand for using Blazor to build non-web UIs. Microsoft has also announced the future evolution of Xamarin Forms, the Multi-platform App UI framework known as .NET MAUI. This framework will allow developers to build native apps which run on Windows, macOS, iOS and Android. According to the roadmap, Blazors programming model will be offered as an option for building these new .NET MAUI apps. This really makes Blazor a compelling technology to learn as once understood, could allow developers to build UIs for almost any platform or device. Hopefully, you can already see Blazor is an exciting technology with a lot of potential. But there is a key concept which is important to understand before we go any further, that of *hosting models.* Let’s tackle that next. ### Understanding hosting models When first getting started with Blazor you will immediately come across the concept of *hosting models*. Essentially, hosting models are where a Blazor application is run. Currently, Blazor has two production supported hosting models called *Blazor WebAssembly* and *Blazor Server*. Regardless of which of these models you choose for your application, the component model is the same meaning components are written the same way and can be interchanged between either hosting model. ![Blazor has a separation between hosting models and its app/component model. Meaning components written for one hosting model can be used with another. - Getting started with C# and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0004.png?w=640&ssl=1)Blazor has a separation between hosting models and its app/component model. Meaning components written for one hosting model can be used with another. The above image shows an abstract representation of Blazors architecture, with the separation between the app and component model and the various hosting models. One of the interesting aspects of Blazor is the potential of other hosting models being made available over time to allow Blazor to run in more places and be used to create more types of UI. Outside of the two production hosting models we will cover below, there are also two other experimental models Microsoft have been testing, Blazor Electron and Mobile Blazor Bindings. #### Blazor Electron Blazor Electron is the oldest of the two experiments and allows Blazor components to be hosted in an Electron application (). Developers write components for this model using HTML and C# in the exact same way as they would for Blazor WebAssembly or Blazor Server. #### Code example An example of a component which can be used by all three of hosting models is shown in the following code. ``` Current count: @currentCount Click me @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } } ``` #### Mobile Blazor Bindings The newer experiment is Mobile Blazor Bindings. This model allows developers to write native mobile applications using Blazors programming model. However, this hosting model can’t use components written using web technologies, components for this hosting model must be written using native controls. The following code contains the same component as the code abode but rewritten for the Mobile Blazor Bindings hosting model. ``` Current count: @currentCount Click me @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } } ``` As you can see the programming model is the same between the two code samples. The logic in the code block is unchanged, it’s just C# after all. The only difference is in the markup where web technologies have been swapped for native mobile controls. This does mean that we can’t swap component around between web-based hosting models and native hosting models. However, once we’ve mastered Blazors programming model we can easily use that knowledge to create other types of UI. Now we’ve talked a little about hosting models in general we’re going to focus in on the two production supported options available in Blazor today, Blazor WebAssembly and Blazor Server. ### Blazor WebAssembly Blazor WebAssembly is the principal hosting model for Blazor applications. Choosing this option will mean your application will run entirely inside the client’s browser making it a direct alternative to JavaScript SPA (Single Page Application) frameworks. To understand how this hosting model works we’re going to walk through the process of initializing a Blazor WebAssembly application shown in following image. ![Bootup of a Blazor WebAssembly application showing the interactions between the client’s browser and the web server - Getting started with C# and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0005.png?w=640&ssl=1)Bootup of a Blazor WebAssembly application showing the interactions between the client’s browser and the web server #### Process begin The process begins when a browser makes a request to the webserver. The web server will return a set of files needed to load the application, these include the host page for the application, usually called index.html, any static assets required by the application such as images, CSS and JavaScript. As well as a special JavaScript file called *blazor.webassembly.js*. At this point, you may be wondering why we have a JavaScript file, one of the big selling points of Blazor is the ability to write UI logic using C# instead of JavaScript, right? Yes, that’s true. But as of right now WebAssembly has a fairly large limitation, it can’t alter the DOM or call Web APIs directly. #### DOM manipulation In order to manage this current limitation, part of the Blazor framework resides in JavaScript called *blazor.webassembly.js* file. This part of the framework does three main things: 1. Loads and initializes the Blazor application in the browser. 2. Provides direct DOM manipulation so Blazor can perform UI updates. 3. Provides APIs for JavaScript interop scenarios, which we’ll discuss in detail in later chapters. It’s possible that in the future this file will no longer be required, this will depend on how fast features are added to WebAssembly and adopted by browsers. But for now, it’s an essential part of the framework. Now, we’ve cleared that up let’s get back to our booting Blazor app. I want to point out that the server returns all static files. They haven’t required any server-side compilation or manipulation. This means that they can be hosted on any service which offers static hosting, there is no requirement for a .NET runtime to be present on the server. For the first time this opens up free hosting options such as GitHub pages to .NET developers (applies to standalone Blazor WebAssembly applications only). #### blazor.boot.json Once the browser has received all the initial files from the web server it can process them and construct the Document Object Model (DOM). Next, *blazor.webassembly.js* is executed. This performs many actions but in the context of starting a Blazor WebAssembly app it downloads the *blazor.boot.json* file. This file essentially contains an inventory of all of the framework and application files which are required to run the app. Most of these files are normal .NET assemblies, there is nothing special about them and they could be run on any compatible .NET runtime. But there’s also another type of file which is downloaded called *dotnet.wasm*. #### dotnet.wasm The *dotnet.wasm* file is in fact a complete .NET runtime, the mono .NET runtime to be exact, which has been compiled to WebAssembly. At this point in time, only the .NET runtime is compiled to WebAssembly, the framework and application are standard .NET assemblies. In the future a feature called AOT (Ahead Of Time) compiling will be introduced which will allow developers to compile parts of their applications into WebAssembly. The benefit of this will be performance, any code compiled to WebAssembly will be many times more performant than the interpreted approach used today. However, there’s a tradeoff, and that’s size. AOT compiled code will be bigger than the standard assemblies meaning a larger overall download size for the application. Once the *blazor.boot.json* file has been downloaded and the files listed in it have been downloaded, it’s time for the application to be run. The WebAssembly .NET runtime is initialized which in turn loads the Blazor framework and finally the application itself. At this point we have a running Blazor application which exists entirely inside the client’s browser. Aside from requesting additional data (if applicable), there’s no further reliance on the server. #### Calculating UI Updates We now understand how a Blazor WebAssembly application boots up. But how do UI updates get calculated? Just as we did for the initialization process, we’re going to follow a scenario to understand how this happens and what Blazor does. ![The process of client-side navigation in Blazor WebAssembly from clicking a link to the application of UI updates - Getting started with C# and Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0006.png?resize=640%2C359&ssl=1)The process of client-side navigation in Blazor WebAssembly from clicking a link to the application of UI updates For our scenario we have a Blazor WebAssembly application with two pages, *home* and *counter*. Neither of these pages have anything on them except a heading saying either “Home” or “Counter”, respectively. The user is on the home page of the application and is going to click on a link to the go to the counter page. We’ll follow the process Blazor goes through to update the UI from that of the home page to the counter page. ### Process explained When the user clicks on the counter link, the navigation event is intercepted by Blazor on the JavaScript side. This event is then passed over to Blazor on the WebAssembly side and is processed by Blazors router component. The router checks its routing table for any routable components which match the link the user has attempted to navigate to. In our case, it will find a match with the Counter component and a new instance of that component will be created and the relevant lifecycle methods will be executed. Once complete Blazor will work out the minimum amount of changes that are required to update the DOM to match that of the Counter component. When this is complete, those changes will be passed back down to the Blazor JavaScript runtime and that will in-turn, apply those changes to the physical DOM. At this point the UI will update the user will be on the Counter page. All of this has happened client-side in the user browser. There was no need for a server during any point in this process. It’s fair to say that in a real world application, you would probably make a call out to a server to some point in this process. This usually happens during the execution of the lifecycle methods of the component being navigated to in order to load some initial data for the component. But this would depend on the individual application. #### Benefits Now we know a bit more about how the Blazor WebAssembly hosting model works, let talk about the benefits and tradeoffs of choosing this model. Let’s start with the benefits. - **Applications run on the client**. This means that there is much less load on the server, you can offload much of the work to the client. This could lead to significant cost saving on server infrastructure and improve the scalability of an application. - **Can work in offline scenarios**. As the app runs entirely inside the browser there’s no need for a persistent connection to the server, making applications more tolerant to unstable network connections. It’s also trivial is enable Progressive Web Application (PWA) functionality. In fact, Blazor WebAssembly has this as an option you can select when creating your application. - **Deployed as static files**. As Blazor WebAssembly apps are just static files, they can be deployed anywhere static hosting is available. This opens up some options which have never been available to .NET developers historically. Services such as GitHub pages, Netlify, Azure Blob Storage, AWS S3 Buckets, Azure Static Web Sites, are all options for hosting standalone Blazor WebAssembly applications. - **Code Sharing**. Potentially one of the greatest benefits with Blazor WebAssembly is if you’re using C# on the server. You can now use the same C# objects on your client as you use on the server. The days of keeping TypeScript models in sync with their C# equivalent and vice versa, are over. #### Tradeoffs Of course, nothing is a silver bullet so let’s understand some tradeoffs of this model. - **Payload**. The initial download size for a Blazor WebAssembly app can be considered quite large. The project template weighs in at around 1.8mb when published. This is largely down to the fact Blazor needs to ship an entire .NET runtime to the client which comes in at around 600kb. However, this is a one-time cost as the runtime and many of the framework assemblies are cached on the first load. Meaning subsequent loads can be a small as a few kb. - **Load time**. A knock-on effect of the payload size can be load time. If the user’s on a poor internet connection the amount of time required to download the initial files will be higher, which will delay the start of the application, leaving the user with a loading message of some kind. This can be offset slightly by using server-side prerendering, however, while this will give the user something more interesting to look at initially, the app still won’t be interactive until all files have been downloaded and initialized. Server-side prerendering for Blazor WebAssembly apps also requires a ASP.NET Core element on the server, which negates any free hosting options. - **Restricted runtime**. This is arguably not a tradeoff as such, but for existing .NET developers who are used to having a relatively free rein over the machine their apps run on, it’s something to be aware of. WebAssembly applications run in the same browser sandbox as JavaScript applications. This means, for example, that you will not be allowed to reach out to the users’ machine and do things such access the local file system. ### Blazor WebAssembly summarize To summarize, Blazor WebAssembly is the hosting model to choose if you’re looking for a direct replacement for a JavaScript SPA framework such as Angular, React or Vue. While there are a few tradeoffs to consider, there are some substantial benefits to choosing this model. ### Blazor Server Now we’ve seen how Blazor WebAssembly works, let’s turn our attention to the Server hosting model and see how it differs. Blazor Server was the first production supported hosting model for Blazor, being released around 8 months before the WebAssembly version. As we did with the previous model, we’ll walk through initializing a Blazor Server application to help us understand how things work. ![Bootup process of a Blazor Server application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0007.png?w=640&ssl=1)Bootup process of a Blazor Server application #### Process begins The process begins with a request to load the site from the browser. When this request hits the webserver two things could happen, the app is started up, or if the app is already running, a new session is established. Why would the app already be running? Blazor WebAssembly follows the traditional SPA model and runs entirely in the browser, essentially making it like a desktop application. Each user has their own instance of the app which runs locally on their machine. Blazor Server is different, only one instance of the application runs on the server, but it can support many clients. Therefore, the app could already be running, and the new request would just establish a new session. #### Process static files The request is then processed by the application and the initial payload is sent back to the browser. This includes static assets such as CSS and JavaScript files, and images. There is also the initial HTML, but this is compiled rather than static HTML we saw in Blazor WebAssembly. The reason for this is that the hosting page for a Blazor Server application is a *Razor Page* rather than a static HTML page in the WebAssembly model. The advantage of this is it allows Blazor Server applications to use server-side prerendering out of the box. In fact, this feature is enabled by default when you create this type of Blazor application. Once the initial payload is returned to the browser the files are processed and the DOM is created – then a file called *blazor.server.js* is executed. The job of this runtime is to establish a SignalR connection back to the Blazor application running on the server. At this point the application is ready for user interaction. #### Calculating UI updates What happens when a user interacts with the application? We saw earlier that in Blazor WebAssembly the events are processed right there in the browser along with calculating any UI updates and applying them to the DOM. But that can’t happen here as the application is running on the server. We’ll follow the same scenario as we did with Blazor WebAssembly, we have a Blazor Server application with two pages, *home* and *counter*. Neither of these pages have anything on them except a heading saying either “Home” or “Counter”, respectively. The user is on the home page of the application and is going to click on a link to the go to the counter page. We’ll follow the process Blazor goes through to update the UI from that of the home page to the counter page. ![Process of updating the UI in Blazor Server](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/04/01_img_0008.png?w=640&ssl=1)Process of updating the UI in Blazor Server #### Process explained The user clicks on the link in the menu and the click event is intercepted by Blazor’s runtime on the client. The runtime then processes the event to understand what has happened. In this case there are two things, a mouse click event and a navigation event, due to it being a hyperlink that was clicked. These two events are then bundled up and sent back to the server over the SignalR connection that was established when the application started. So, the client sent a the message to the server and the server unpacks and process the message. The Blazor framework then calls any application code necessary. In this case it would instantiate an instance of the counter page component and execute the relevant lifecycle methods. #### SignalR Once complete, Blazor will work out what the minimum amount of changes needed to make the current page transform to the counter page and then send these back to the client via the SignalR connection. Just to be clear, Blazor will not send back an entirely new page to the client. It will only send back the minimum number of instructions needed to update the current DOM to match the Counter page. In our case, the only difference is the heading. Blazor will send back a single instruction to change the text in the heading from “Home” to “Counter”. #### DOM Once back on the client, the client unpacks the changes, and the required changes are applied to the physical DOM. From the user’s perspective, they appear to have navigated to a new page in the application, the counter page. But they are still on the same physical page, it just has a different header. You may have spotted this already, but the overall process isn’t any different to how Blazor WebAssembly worked, it’s just been stretched out a bit over that SignalR connection. Blazor Server is just as much a SPA as Angular, Vue or Blazor WebAssembly. It just happens to run its logic and calculate UI updates on the server instead of the client. In fact, I would go as far as saying if you were presented with two identical applications, one written in Blazor Server and one in Blazor WebAssembly, you wouldn’t be able to tell the difference between them, as a user. #### Performance Before we talk about benefits and tradeoffs for this model, I want quickly mention performance. With all the network chatter which goes on in this hosting model I’m sure it may have crossed your mind that this might not scale particularly well. #### The test In 2019, the ASP.NET Core team did some testing to establish the performance levels of Blazor Server apps. They setup an application in Azure and tested it on different powered virtual machines, checking the number of *active* users the application could support. Here are the results. - Standard D1 v2 Instance (1vCPU & 3.5GB Memory). Over 5000 concurrent users - Standard D3 v2 Instance (4vCPU & 14GB Memory). Over 20,000 concurrent users As you can see, Blazor Server is no slouch when it comes to performance. The main factor they found which effects the number of clients that can be supported is memory. This makes sense as the server needs to keep track of all the clients which are connected to it, the more there are the more information needs to be stored in memory. ##### Testing The other major finding from testing was how network latency effected the application. As all interaction are sent back to the server for processing, latency can have a large impact on usability. If the server is located 250ms away from the client, then each interaction is going to take at least 500ms to be processed as it has to travel to the server (250ms), then be processed, then travel back again (250ms). Testing found that when the latency went above 200ms then the UI began to feel sluggish and less responsive. As a rough rule you would always want your users to be on the same continent as the server. If you want to have a globally available Blazor Server application, then you need to have your app evenly distributed across the world aiming to keep all clients within 200ms of a server. #### Benefits As we did before, let’s look at the benefits and tradeoffs of choosing a Blazor Server application. - **Small payload**. As the application is running on the server as opposed to the client, the initial download is significantly smaller. Depending on static assets such as CSS and images a Blazor Server application can be as small as a 100-200kb. - **Fast load time**. With a much smaller payload the application loads much faster. The server-side prerendering also helps as the user never sees a loading message. - **Access to the full runtime**. The application code is executing on the server on top of the full .NET runtime. This means that you can do things such as access the servers file system if you require without hitting any security restrictions. - **Code security**. If you have code which is proprietary, and you don’t want people being able to download and interrogate it then Blazor Server is a good choice. The application code is all executed on the server and only the UI updates are sent to the client. This means your code is never exposed to the client in anyway. #### Tradeoffs - **Heavy server load**. Where Blazor WebAssembly allows us to utilize the power of the client Blazor Server does the complete opposite. Almost all of the work is now being performed by the server. Meaning you might need a larger investment in your infrastructure to support Blazor Server apps. - **Doesn’t work offline**. Where Blazor WebAssembly takes offline working in its stride Blazor Server does not. The SignalR connection is the lifeline of the application and without it the client can’t function at all. By default, this results in an overlay with a message saying the client is attempting to reestablish the connection. If this fails, the user has to refresh the browser to restart the application. - **Latency**. Due to its design Blazor Server apps are sensitive to latency issues. Every interaction the user has with the application must be sent back to the server for processing and await any updates that need to be applied. If there is a high latency in the connection between client and server a noticeable lag manifests in the UI and actions quickly feel sluggish. In real numbers a latency above 200ms is going to start causing these issues. - **Requires a stable connection**. Following on from the need for low latency and tying in with the inability to work offline. Blazor Server apps need to have a stable internet connection. If the connection is intermittent in any way, the user will continually see the reconnecting overlay in their application which quickly becomes very disruptive. An obvious scenario where this could occur is when a user is on a mobile device which has intermittent connection. #### Blazor Server summarize In summary, if you’re looking for a fast loading application and you have users with a fast and stable network connection, then Blazor Server is a great choice. **Categories:** .NET Core, .NET General, Blazor, C#, Microsoft, Visual Studio, WebAPI **Tags:** blazor, blazor-server, blazor-webassembly, webassembly --- ### [NET8 is announced](https://puresourcecode.com/dotnet/net8-is-announced/) **Published:** March 27, 2023 **Author:** Enrico **Excerpt:** Microsoft has recently announced the release of NET8, the latest version of its popular software development platform **Content:** Microsoft has recently announced the release of NET8, the latest version of its popular software development platform. NET8 brings many new features and improvements to help developers create modern applications for web, mobile, desktop, cloud, and IoT. In this blog post, we will present some of the highlights of NET8 and how they can benefit you as a developer. Can you believe it? It feels like just yesterday that we were geeking out over NET 7, and here we are already talking about .NET 8! Time flies when you’re coding up a storm and this first preview and the NET 8 Preview 2 are already released. ## The new MAUI One of the main goals of NET8 is to simplify and unify the development experience across different platforms and devices. NET8 introduces a new project system called **NET Multi-platform App UI** (**MAUI**), which allows you to build native UIs for Windows, Mac, Android, and iOS using a single codebase and project file. You can use C# and XAML to design your UIs and share code and resources across platforms. [NET MAUI](https://puresourcecode.com/tag/maui/) also integrates with Visual Studio and Visual Studio Code, providing you with tools such as IntelliSense, debugging, hot reload, and hot restart. ## Support Blazor WebAssembly AOT Another key feature of NET8 is the support for **Blazor WebAssembly AOT** (ahead-of-time) compilation. Blazor is a framework that lets you build interactive web applications using [C#](https://puresourcecode.com/category/dotnet/csharp/) and HTML. [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) runs your C# code directly in the browser using a WebAssembly-based .NET runtime. With Blazor WebAssembly AOT, you can compile your C# code to native code before deploying it to the web server, resulting in faster startup times and better performance. Blazor WebAssembly AOT also enables you to use native libraries and interop with JavaScript. ## Other improvements NET8 also improves the performance and reliability of your applications by introducing new features such as Source Generators, global usings, file-scoped namespaces, implicit usings, minimal [APIs](https://puresourcecode.com/tag/api/), and improved garbage collection. **Source Generators** are a new way to generate code at compile time based on your source code or metadata. They can help you reduce boilerplate code, optimize performance, and enhance tooling. **Global usings** allow you to specify namespaces that are automatically imported in every source file in your project, saving you from typing them repeatedly. **File-scoped namespaces** and **implicit usings** simplify the syntax of your [C#](https://puresourcecode.com/category/dotnet/csharp/) code by reducing the indentation and removing unnecessary keywords. Minimal APIs enable you to create web APIs with minimal code and configuration using a new set of extension methods for ASP.NET Core. Improved garbage collection reduces memory usage and pauses by introducing new modes such as concurrent compacting GC and pinned object heap compaction. These are just some of the exciting features that NET8 has to offer. If you want to learn more about NET8 and how to get started with it, you can visit the [official website](https://dotnet.microsoft.com/) or check out the [documentation](https://docs.microsoft.com/en-us/dotnet/). You can also download NET8 from [this page](https://dotnet.microsoft.com/download/dotnet/8.0) or use Visual Studio 2023 or Visual Studio Code with the latest updates. ## dotnet publish and dotnet pack Microsoft just released an awesome new feature for the `dotnet publish` and `dotnet pack` commands that makes it even easier to produce production-ready code. Before this update, these commands produced `Debug` assets by default, which could be a bit of a hassle if you wanted to produce production-ready code. But now, with the new update, `dotnet publish` and `dotnet pack` **produce `Release` assets by default**, which means you can easily produce production-ready code without any extra steps. But don’t worry, if you still need to produce `Debug` assets for any reason, **it’s still possible to do** so by setting the in `false` the `PublishRelease` property. ### How dotnet publish and dotnet pack works? First, let’s create a new console application using the `dotnet new console` command. Then, let’s build the project using `dotnet build` and take a look at the output. In this case, the output will be in `Debug` mode, since that’s the default behavior of `dotnet build`. Next, let’s run the `dotnet publish` command to produce production-ready code. With the new update, this command will now produce `Release` assets by default, which is exactly what we want for production code. We can see the `Release` assets in the `/app/bin/Release/net8.0` directory. Finally, let’s say we need to produce `Debug` assets for some reason. We can do that by running the `dotnet publish` command again, but this time we’ll set the `PublishRelease` property to `false`. This will produce `Debug` assets instead of `Release` assets, which we can see in the `/app/bin/Debug/net8.0` directory. And that’s it! With this new feature, it’s now easier than ever to produce production-ready code using the `dotnet publish` and `dotnet pack` commands. Here the documentation [on Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#dotnet-publish-and-dotnet-pack). ## Improvements in System.Text.Json serialization In this NET 8 preview 1, `System.Text.Json` is a built-in .NET library that provides JSON serialization and deserialization functionality. It allows developers to convert .NET objects to JSON data and vice versa. Now, `System.Text.Json` serialization and deserialization functionality has been **improved in various ways for NET8**. Another series of improvements included in this preview are in the source generator when used with ASP.NET Core in Native AOT apps, making it more reliable and faster. Additionally, the source generator will support in .NET 8 serializing types with `required` and `init properties`, which were already supported in reflection-based serialization. Moreover, customization of serialization for members that aren’t present in the JSON payload is now possible. Lastly, properties from interface hierarchies can now be serialized, including those from both the immediately implemented interface and its base interface. Let’s check this example: ``` IDerived value = new DerivedImplement { Base = 0, Derived =1 }; JsonSerializer.Serialize(value); // {"Base":0,"Derived":1} public interface IBase { public int Base { get; set; } } public interface IDerived : IBase { public int Derived { get; set; } } public class DerivedImplement : IDerived { public int Base { get; set; } public int Derived { get; set; } } ``` Now, the `JsonNamingPolicy` has been expanded to include naming policies for `snake_case` (with an underscore) and `kebab-case` (with a hyphen) property name conversions. These new policies can be utilized in the same way as the `JsonNamingPolicy.CamelCase` policy. ``` var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower }; // { "property_name" : "value" } JsonSerializer.Serialize(new { PropertyName = "value" }, options); ``` The `JsonSerializerOptions.MakeReadOnly()` method gives you explicit control over when a `JsonSerializerOptions` instance is frozen. You can also check whether it’s read-only with the `IsReadOnly` property. More info on the official documentation on [System.Text.Json serialization](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#systemtextjson-serialization) ## GetItems() The `GetItems()` method is a new feature in .NET 8 that allows you to randomly select a specific number of items from a given set of elements. This can be useful in games, simulations, and other applications where randomness is desired. The method is available in both `System.Random` and `System.Security.Cryptography.RandomNumberGenerator`. In this example, we have an array of `City` objects, and we use the `GetItems()` method to randomly select 3 cities from the array: ``` private static ReadOnlySpan s_allCities = new[] { new City("New York", "USA"), new City("London", "UK"), new City("Paris", "France"), new City("Tokyo", "Japan"), new City("Sydney", "Australia"), }; ... City[] selectedCities = Random.Shared.GetItems(s_allCities, 3); foreach (City city in selectedCities) { Console.WriteLine(city.Name + ", " + city.Country); } // Output: // Paris, France // Tokyo, Japan // Sydney, Australia ``` We then loop through the selected cities and print their name and country to the console. The output will be different each time we run the program, because the cities are selected randomly. More info on the official documentation on [GetItems()](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#getitemst). ## Shuffle() The `Shuffle()` method is another new feature in .NET 8 that allows you to randomize the order of elements in a span. This is important in machine learning instances when you wish to eliminate training bias by randomizing training data order. Here’s an example that shows how to use `Shuffle()` with an array of `YourType` objects: ``` YourType[] trainingData = LoadTrainingData(); Random.Shared.Shuffle(trainingData); IDataView sourceData = mlContext.Data.LoadFromEnumerable(trainingData); DataOperationsCatalog.TrainTestData split = mlContext.Data.TrainTestSplit(sourceData); model = chain.Fit(split.TrainSet); IDataView predictions = model.Transform(split.TestSet); // ... ``` In this example, we load some training data into an array of `YourType` objects and use `Random.Shared` to shuffle the order of the elements. We then load the shuffled data into an `IDataView` object, split the data into training and test sets, and use the shuffled training data to train a machine learning model. Finally, we use the trained model to make predictions on the test set. More info on the official documentation on [Shuffle()](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#shufflet) ## Performance Improvements .NET 8 introduces several new types that are focused on improving app performance. These new types are: - The `System.Collections.Frozen` namespace includes two collection types, `FrozenDictionary` and `FrozenSet`. These types do not allow any changes to keys and values once a collection is created, which allows for faster read operations such as `TryGetValue()`. They are particularly useful for collections that are populated on first use and then persisted for the duration of a long-lived service: ``` private static readonly FrozenDictionary s_configurationData = LoadConfigurationData().ToFrozenDictionary(optimizeForReads: true); // ... if (s_configurationData.TryGetValue(key, out bool setting) && setting) { Process(); } ``` - The `System.Text.CompositeFormat` type is useful for optimizing format strings that aren’t known at compile time. A little extra time is spent up front to do work such as parsing the string, but it saves the work from being done on each use: ``` private static readonly CompositeFormat s_rangeMessage = CompositeFormat.Parse(LoadRangeMessageResource()); // ... static string GetMessage(int min, int max) => string.Format(CultureInfo.InvariantCulture, s_rangeMessage, min, max); ``` The `System.Buffers.IndexOfAnyValues` type is designed to be passed to methods that look for the first occurrence of any value in the passed collection. .NET 8 adds new overloads of methods like `String.IndexOfAny` and `MemoryExtensions.IndexOfAny` that accept an instance of the new type. More info see the official documentation on [Performance-focused types](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#performance-focused-types) ## Native AOT .NET 8 brings improvements to the **native ahead-of-time** (AOT) compilation feature that was first introduced in .NET 7. Publishing an application as native AOT generates a self-contained version of the app that doesn’t require a runtime as everything is included in a single file. In addition to the existing support for various platforms, .NET 8 now includes **support for the x64 and Arm64 architectures on macOS**. This means that developers can now publish their .NET apps as native AOT for macOS systems. The latest improvements to native AOT apps on Linux systems have resulted in significantly reduced application sizes. According to recent tests, native AOT apps built with .NET 8 Preview 1 now take up to **50% less space** compared to those built with .NET 7. You can see in the table below a comparison of the size of a “Hello World” app published with native AOT and including the entire .NET runtime between the two versions: Operating System.NET 7.NET 8 Preview 1Linux x64 (with -p:StripSymbols=true)3.76 MB1.84 MBWindows x642.85 MB1.77 MBThese improvements in native AOT can help .NET developers to create smaller, faster, and more efficient apps that run on a variety of platforms without requiring any external dependencies. More info see the official documentation on [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#native-aot) ## Code generation .NET 8 also has some improvements in **code generation and JIT** (Just-In-Time) compilation enhancing performance and efficiency: - **Arm64 architecture** performance improvements - **SIMD** (Single Instruction Multiple Data) **improvements** for better vectorization and parallelization of operations - **Cloud-native improvements** for better performance in containerized environments - **Profile-guided optimization** (PGO) **improvements** that enable better optimizations based on application usage patterns - **Support for AVX-512 ISA extensions** for more efficient floating-point operations on modern CPUs - **JIT** (Just-In-Time) t**hroughput improvements** for faster code generation - Loop and general optimizations that improve the performance of frequently used code blocks These improvements help developers to optimize the performance of their .NET applications and reduce resource utilization in cloud-native environments. More info see to the official documentation on [Code generation](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#code-generation) ## .NET container images .NET 8 also makes a few changes to the way .NET container images work. First, Debian 12 (Bookworm) is now the default Linux distribution in the container images. Additionally, the images include a `non-root` user to make the images `non-root` capable. To run as `non-root`, add the line `USER app` at the end of your Dockerfile or a similar instruction in your Kubernetes manifests. The **default port has also changed** from `80` to `8080` and a **new environment variable** `ASPNETCORE_HTTP_PORTS` is available to make it **easier to change ports**. The format for the `ASPNETCORE_HTTP_PORTS` variable is easier compared to the format required by `ASPNETCORE_URLS`, and it accepts a list of ports. If you change the port back to `80` using one of these variables, it won’t be possible to run as `non-root`. To pull the .NET 8 Preview SDK, you can use the following tag which includes the `-preview` suffix in the tag name for preview container images: ``` docker run --rm -it mcr.microsoft.com/dotnet/sdk:8.0-preview ``` The suffix `-preview` will no longer be used for release candidate (RC) releases. In addition, developers can use chiseled Ubuntu images with .NET 8 that offer a smaller attack surface, no package manager or shell, and `non-root` capability. These images are ideal for developers looking for the advantages of appliance-style computing. **Categories:** .NET, .NET8, Microsoft, News **Tags:** dotnet, microsoft, net8 **Hashtags:** net8 --- ### [Blazor component for ChartJS](https://puresourcecode.com/dotnet/blazor/blazor-component-for-chartjs/) **Published:** May 6, 2022 **Author:** Enrico **Excerpt:** I finally complete the first implementation of my Blazor component for ChartJS that helps you to create beautiful graphs **Content:** After a lot of work, I finally complete the first implementation of my [Blazor component](https://puresourcecode.com/tag/blazor-component/) for [ChartJS](https://www.chartjs.org/) for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). This component helps you to create beautiful graphs with the famous JavaScript library for chart called ChartJS. Following a couple of experiments with ChartJS - [Using ChartJS with Blazor](https://puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/) - [Chart.js Asp.net : Create Pie chart with database jQuery Ajax C#](https://puresourcecode.com/dotnet/asp-net/chart-js-asp-net-create-pie-chart-with-database-jquery-ajax-c/) - Add labels and a new `OnChartClick`: more details on this [post](https://puresourcecode.com/dotnet/blazor/labels-and-onclickchart-for-chartjs/) I decided to start to write a faced for Blazor. There is a project on GitHub, but it is old and not maintained any more. The demo site is live [here](https://chartjs.puresourcecode.com/). The source code of the component and the demo is available on [GitHub](https://github.com/erossini/BlazorChartjs). ## My component So, I restarted from scratch using [NET6](https://puresourcecode.com/tag/net6/) and the latest version of ChartJS at the present the version 3.7.1. With the current implementation, you can create the following charts: - Area - Bar - Bubble - Doughnut - Pie - Line - Polar Area - Radar - Scatter To use the Blazor component for ChartJS in your Blazor WebAssembly or Blazor Server project, the first thing is to add the ChartJS library and the JavaScript for the component in your `Index.html`. Before closing the tag `body`, add the following lines ``` ``` I added the ChartJS library in the component, so, if you use the version from the component, you know the generation of the charts is correct and working. Maybe with the next versions of the library, I have to change the model. Then, you have to add in your `_Imports.razor` the following namespaces ``` @using PSC.Blazor.Components.Chartjs @using PSC.Blazor.Components.Chartjs.Enums @using PSC.Blazor.Components.Chartjs.Models @using PSC.Blazor.Components.Chartjs.Models.Common @using PSC.Blazor.Components.Chartjs.Models.Bar @using PSC.Blazor.Components.Chartjs.Models.Bubble @using PSC.Blazor.Components.Chartjs.Models.Doughnut @using PSC.Blazor.Components.Chartjs.Models.Line @using PSC.Blazor.Components.Chartjs.Models.Pie @using PSC.Blazor.Components.Chartjs.Models.Polar @using PSC.Blazor.Components.Chartjs.Models.Radar @using PSC.Blazor.Components.Chartjs.Models.Scatter ``` So, how you can see, there is a namespace for each type of charts plus the generics (`Enums`, `Models` and the base). This allows you to use the component across your application. ## Create the first bar graph Now, in your Blazor project, create a new Razor Component and add this line ``` ``` `Chart` is the common name for the Blazor component for ChartJS. Only one component for all the charts. Now, in the `code` add this code ``` @code { private BarChartConfig _config1; private Chart _chart1; protected override async Task OnInitializedAsync() { _config1 = new BarChartConfig() { Options = new Options() { Plugins = new Plugins() { Legend = new Legend() { Align = LegendAlign.Center, Display = false, Position = LegendPosition.Right } }, Scales = new Scales() { X = new XAxes() { Stacked = true, Ticks = new Ticks() { MaxRotation = 0, MinRotation = 0 } }, Y = new YAxes() { Stacked = true } } } }; _config1.Data.Labels = BarDataExamples.SimpleBarText; _config1.Data.Datasets.Add(new BarDataset() { Label = "Value", Data = BarDataExamples.SimpleBar.Select(l => l.Value).ToList(), BackgroundColor = Colors.Palette1, BorderColor = Colors.PaletteBorder1, BorderWidth = 1 }); } } ``` When the page is initialized in the `OnInitializedAsync` I create the configuration for the chart. For the options, please refer to the ChartJs documentation. This is the result of this code. ![Bar chart with the Blazor component for ChartJS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image.png?resize=640%2C339&ssl=1)Bar chart with the Blazor component for ChartJS ### Use the right Chart configuration Each chart has its `ChartConfig` to use. ChartChart configBarBarChartConfigBubbleBubbleChartConfigDoughnutDoughnutChartConfigLineLineChartConfigPiePieChartConfigPolarPolarChartConfigRadarRadarChartConfigScatterScatterChartConfigFor more information about the chart configuration, look at the [demo website](https://chartjs.puresourcecode.com/) (where there is for each graph the code) or the source code on [GitHub](https://github.com/erossini/BlazorChartjs). ## How to fix the size of the graph with ChartJS So, this is a very common problem with ChartJs. This is an example of the issue. Although, you add the style to the chart, the chart removes all your settings and the result is not what you expect. ![Issue with the size of the graph with ChartJS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-1.png?resize=640%2C466&ssl=1)Issue with the size of the graph with ChartJS If you don’t use my component, you have to wrap the `canvas` in a `div` and add the `style` like that ``` ``` Then, in the chart options, you have to set ``` new Chart(ctx, { // ... other config ... options: { responsive: true, maintainAspectRatio: false } }); ``` If your wrapper doesn’t have a relative size, you should be able to *dynamically* change the height of the chart by changing the height of the wrapper: ``` document.getElementById("wrapper").style.height = '128px'; ``` ## How to fix the size of the graph with the Blazor component for ChartJS If you use my component, it is easier. In the `ChartConfig` you have to add those lines: ``` _config1 = new LineChartConfig() { Options = new Options() { Responsive = true, MaintainAspectRatio = false } }; ``` Adding `Responsive` and `MaintainAspectRatio` to the chart configuration, the chart will be displayed with the size you want. So, the result is like in the following screenshot. ![The chart size is fixed with Blazor component for ChartJS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-2.png?resize=640%2C453&ssl=1)The chart size is fixed with Blazor component for ChartJS ## Wrap up In conclusion, this is my new Blazor component for ChartJS that you can use in your projects. Few important links: - [Demo website](https://chartjs.puresourcecode.com/) - Source code on [GitHub](https://github.com/erossini/BlazorChartjs) - [Support forum](https://puresourcecode.com/forum/chart-js-for-blazor/) **Categories:** Blazor **Tags:** blazor-component, blazor-server, blazor-webassembly --- ### [Labels and OnClickChart for ChartJs](https://puresourcecode.com/dotnet/blazor/labels-and-onclickchart-for-chartjs/) **Published:** March 15, 2023 **Author:** Enrico **Excerpt:** In this new post, I introduce an update with labels and OnClickChart event, my ChartJs component for Blazor. Let me know what you think **Content:** In this new post, I introduce an update with **labels** and **OnClickChart** event, my [ChartJs component for Blazor](https://puresourcecode.com/dotnet/blazor/blazor-component-for-chartjs/) ([Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/)). This component helps you to create beautiful graphs with the famous JavaScript library for chart called ChartJS. This component is available on: - [Nuget](https://www.nuget.org/packages/PSC.Blazor.Components.Chartjs/) - [GitHub](https://github.com/erossini/BlazorChartjs) If you need support, please use the [Forum](https://puresourcecode.com/forum/chart-js-for-blazor/). ## Add labels to the chart I added the `chartjs-plugin-datalabels` plugin in the component. This plugin shows the labels for each point in each graph. For more details abour this plugin, visit its [website](https://chartjs-plugin-datalabels.netlify.app/). ![Example of chart with labels - Labels and OnClickChart for ChartJs](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/224721251-da6959de-2b20-4d42-926b-b036de6695ee.png?w=640&ssl=1)Example of chart with labels First, in the *index.html*, we have to add after the `chart.js` script, another script for this component. It is important to add the script for `chartjs-plugin-datalabels` after `chart.js`. If the order is different, the plugin could not work. For example ``` ``` In the code, you have to change the property `RegisterDataLabels` under `Options` to `true`. That asks to the component to register the library if the library is added to the page and there is data to show. For example, if I define a `LineChartConfig` the code is ``` _config1 = new LineChartConfig() { Options = new Options() { RegisterDataLabels = true, Plugins = new Plugins() { DataLabels = new DataLabels() { Align = DatalabelsAlign.Start, Anchor = DatalabelsAnchor.Start, } } } }; ``` With this code, the component will register the library in `chart.js`. It is possible to define a `DataLabels` for the entire chart. Also, each dataset can have its own `DataLabels` that rewrites the common settings. ## OnClickChart When a user click on a chart and in particular on a point with value (bars, point, etc), the event `OnClickChart` returns the dataset index, the value index in the dataaet and the value. For example, in this chart the function `OnClickChart` is called in the event of `OnChartClick`. ``` ``` The function receives `ClickValue` as parameter that contains the 3 values. ``` public async Task OnClickChart(ClickValue value) { ClickString = $"Dataset index: {value.DatasetIndex} - Value index: {value.ValueIndex} " + $"- Value: {value.Value}"; } ``` With this code, if the user clicks on a point, the function writes the values in the page. ![Example of a chart with OnChartClick - Labels and OnClickChart for ChartJs](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/225041631-805cf3c6-4b3f-4475-b57e-2a1962472c35.png?w=640&ssl=1)Example of a chart with OnChartClick **Categories:** .NET6, Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly **Hashtags:** blazor-component --- ### [Browser detect component for Blazor](https://puresourcecode.com/dotnet/blazor/browser-detect-component-for-blazor/) **Published:** February 10, 2022 **Author:** Enrico **Excerpt:** In this post, I talk about user-agent and I'm going to create a browser detect component for  Blazor WebAssembly and Blazor Server with .NET6. **Content:** In this post, I’m going to create a browser detect component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/) with .NET6. Firstly, if you need help or info about this component, leave your message in the [Forum](https://puresourcecode.com/forum/browser-detect-for-blazor/). > There is a new version of this component that can detect Windows 11. Read the update on this [post](https://puresourcecode.com/dotnet/blazor/browser-detect-component-for-blazor-2/) First thing to remember, you can try your component by yourself from the website. [Try it now!](https://browserdetect.puresourcecode.com/) Also, the full source code of this component is on [GitHub](https://github.com/erossini/BlazorBrowserDetect). So, the following screenshot is an example of the information the component detects from your browser. ![Browser detection in action on the website - Browser detect component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-14.png?resize=640%2C344&ssl=1)Browser detection in action on the website ## User-Agent detection: limitation and issues `User-Agent: ` is a string of characters sent by HTTP clients (browsers, bots, calendar applications, etc.) for each individual HTTP request to a server. The HTTP Protocol as [defined in 1991](https://www.w3.org/Protocols/HTTP/AsImplemented) didn’t have this field, but the next [version defined in 1992](https://www.w3.org/Protocols/HTTP/HTTP2.html) added [`User-Agent`](https://www.w3.org/Protocols/HTTP/HTRQ_Headers.html#user-agent) in the HTTP requests headers. Its syntax was defined as “the software product name, with an optional slash and version designator“. The prose already invited people to use it for analytics and identify the products with implementation issues. So, fast forward to August 2013, the HTTP/1.1 specification is being revised and also defines [`User-Agent`](https://tools.ietf.org/html/draft-ietf-httpbis-p2-semantics-23#section-5.5.3). > Then, a user agent SHOULD NOT generate a User-Agent field containing **needlessly fine-grained detail** and SHOULD limit the addition of subproducts by third parties. Overly long and detailed User-Agent field values increase request latency and the risk of a user being identified against their wishes (“fingerprinting”). > > Likewise, implementations are encouraged **not to use the product tokens of other implementations in order to declare compatibility with them**, as this circumvents the purpose of the field. If a user agent masquerades as a different user agent, recipients can assume that the user intentionally desires to see responses tailored for that identified user agent, even if they might not work as well for the actual user agent being used. Basically, the HTTP specification discouraged since its inception the detection of the `User-Agent` string for tailoring the user experience. Currently, the user agent strings have [become overly long](https://webaim.org/blog/user-agent-string-history/) and they: - are abused in every possible way - include detailed information. - lie about what they really are and they are used for branding and advertising the devices they run on. ### User-Agent Detection First, user agent detection (or sniffing) is the mechanism used for parsing the `User-Agent` string and inferring physical and applicative properties about the device and its browser. But let get the record straight. User-Agent sniffing is a **future fail strategy**. By design, you will detect only what is known, not what will come. The space of small devices (smartphones, feature phones, tablets, watches, Arduino, etc.) is a very fast-paced evolving space. The diversity in terms of physical characteristics will only increase. Updating databases and algorithms for **identifying correctly is a very high maintenance task** which is doomed to fail at a point in the future. Sites get abandoned, libraries are not maintained and Web sites will break just because they were not planned for the future coming devices. All of these have costs in resources and branding. ### Using capabilities Therefore, new solutions are being developed for helping people to adjust the user experience [depending on the capabilities](https://hacks.mozilla.org/2012/07/the-web-developer-toolbox-modernizr/) of the products, not its name. In addition, responsive design helps to create Web sites that are adjusting for different screen sizes. So, each time you detect a product or a feature, it is important to thoroughly [understand why](https://hacks.mozilla.org/2013/04/detecting-touch-its-the-why-not-the-how/) you are trying to detect this feature. You could fall in the same traps as the ones existing with user agent detection algorithms. For example, looking on the internet, we have to deal on a daily basis with abusive user agent detection blocking Firefox OS and/or Firefox on Android. It is not only Mozilla products, every product and brand has to deal at a point with the fact to be excluded because they didn’t have the right token to pass an ill-coded algorithm. User agent detection leads to situation where a new player can hardly enter the market even if it has the right set of technologies. Remember that there are huge benefits to create a system which is [resilient to many situations](https://christianheilmann.com/2012/02/16/stumbling-on-the-escalator/). For this reason, some companies will be using the `User-Agent` string as an identifier for bypassing a pay-wall or offering specific content for a group of users during a marketing campaign. It seems to be an easy solution at first but it creates an environment easy to by-pass in spoofing the user agent. ## The browser detect component So, after all this consideration, I decided to use a mix of all user-agent parser and capability detection for creating the component. Firstly, how to use the component in your Blazor project. ### Add the component Firstly, add the component using the [NuGet package](https://www.nuget.org/packages/PSC.Blazor.Components.BrowserDetect/). Then, in the page add the following tag ``` ``` Now, add the following the code ``` @code { public BrowserInfo Info { get; set; } } ``` So, the component is adding automatically the JavaScript to the page to run same tests on the browser. Then, the JavaScript collects info about the browser and runs same capabilities functions. After that, it passes the results to the component. Then the component updates the `BrowserInfo` and your page receives all the updates. Easy, that’s it, nothing else. In brief, the variable `BrowserInfo` has all the properties detected from your browser. ### Properties So, this is the list of property the component returns about your browser: same of them are detected parsing the user-agent, other testing the capabilities. PropertyValueBrowserNameName of the browserBrowserMajorMajor version of the browserBrowserVersionVersion of the browserCPUArchitectIf it is possible, the component detect the CPU architecture of the machineDeviceModelDevice model (if it is possible)DeviceTypeDevice type (if it is possible)DeviceVendorDevice Vendor (if it is possible)EngineNameBrowser engine nameEngineVersionBrowser engine versionGPURendererType of the GPU rendererGPUVendorVendor of the GPUIsDesktopDetect if the device is a desktop computerIsMobileDetect if the device is a mobileIsTabletDetect if the device is a tabletIsAndroidDetect if the device is an Android deviceIsIPhoneDetect if the device is an iPhone or iPodIsIPadDetect if the device is an iPad (any version)IsIPadProDetect if the device is an iPad ProOSNameDetect the operating systemOSVersionVersion of the operating systemScreenResolutionDetect the screen resolutionTimeZoneRead the time zoneUserAgentThe full user agent## Wrap up In conclusion, this is the browser detect component for Blazor. Then, please leave your comment in the section “[Browser detect component for Blazor](https://puresourcecode.com/forum/browser-detect-for-blazor/)” in the [Forum](https://puresourcecode.com/forum/). **Categories:** Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly, browsers --- ### [Browser Detect component for Blazor](https://puresourcecode.com/dotnet/blazor/browser-detect-component-for-blazor-2/) **Published:** March 14, 2023 **Author:** Enrico **Excerpt:** I'm releasing a new update for Browser Detect component for Blazor that detect correctly Windows 11 plus few improvements. **Content:** An year ago I released the [Browser detect component for Blazor](https://puresourcecode.com/dotnet/blazor/browser-detect-component-for-blazor/) on [NuGet](https://www.nuget.org/packages/PSC.Blazor.Components.BrowserDetect/). This is a browser detect component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/) with .NET6. Also, it was downloaded almost 20.000 times and used in quite interesting project. Today, I’m releasing a new update that detect correctly `Windows 11` plus few improvements. ## Detect Windows 11 and CPU architecture using User-Agent Client Hints Websites can differentiate between users on Windows 11 and Windows 10, and detect the CPU architecture of the device, by using User-Agent Client Hints (UA-CH). The User-Agent Client Hints format is used by browsers to provide user agent information to websites. Websites can also use the user agent information that’s sent from the browser to detect information such as: - The browser brand. - The browser version number. - The device platform on which the browser is running. There are two approaches for sites to access user agent information: - User-Agent strings (legacy). - User-Agent Client Hints (recommended). For details about these two approaches, see [Detecting Microsoft Edge from your website](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance). In Microsoft Edge (and also in Google Chrome), sites can differentiate between users on Windows 11 and Windows 10, and can detect the CPU architecture of the device, via User-Agent Client Hints (UA-CH). This information can be found in the following UA-CH request headers: Header fieldValues that indicate Windows 10Values that indicate Windows 11`Sec-CH-UA-Platform``Windows``Windows``Sec-CH-UA-Platform-Version`values between `1.0.0` and `10.0.0``13.0.0` and aboveUser-Agent strings won’t be updated to differentiate between Windows 11 and Windows 10, or to differentiate between CPU architectures. We don’t recommend using User-Agent strings to retrieve user agent data. Browsers that don’t support User-Agent Client Hints won’t be able to differentiate between Windows 11 and Windows 10, or between CPU architectures.[](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11#browsers-that-support-user-agent-client-hints) ### Browsers that support User-Agent Client Hints The following table shows which browsers support differentiating between Windows 11 and Windows 10, and between different CPU architectures. BrowserSupports differentiation via User-Agent Client Hints?Microsoft Edge 94+YesChrome 95+YesOperaYesFirefoxNoInternet Explorer 11No[](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11#sample-code-for-detecting-windows-11) ### Sample code for detecting Windows 11 The following code detects Windows 11: ``` navigator.userAgentData.getHighEntropyValues(["platformVersion"]) .then(ua => { if (navigator.userAgentData.platform === "Windows") { const majorPlatformVersion = parseInt(ua.platformVersion.split('.')[0]); if (majorPlatformVersion >= 13) { console.log("Windows 11 or later"); } else if (majorPlatformVersion > 0) { console.log("Windows 10"); } else { console.log("Before Windows 10"); } } else { console.log("Not running on Windows"); } }); ``` ### Sample code for detecting ARM or x86 Use detection of CPU architecture to have your website automatically download the version of your app that’s built specifically for the user’s CPU. CPU detection is particularly helpful for ARM-based devices, so that a customer using an ARM device automatically downloads the native ARM version of an application. This prevents the user from inadvertently installing an app that’s built for x86, and then experiencing reduced performance due to emulation. The following code detects CPU architecture: ``` navigator.userAgentData.getHighEntropyValues(["architecture","bitness"]) .then(ua => { if (navigator.userAgentData.platform === "Windows") { if (ua.architecture === 'x86') { if (ua.bitness === '64') { console.log("x86_64"); } else if (ua.bitness === '32') { console.log("x86"); } } else if (ua.architecture === 'arm') { if (ua.bitness === '64') { console.log("ARM64"); } else if (ua.bitness === '32') { console.log("ARM32"); } } } else { console.log("Not running on Windows"); } }); ``` ### Optimizing detection performance with `Critical-CH` Currently, website servers must send the `Accept-CH` response header to the browser client to request higher entropy fields not sent in the `Sec-CH-UA` header by default. The following diagram shows the browser sending request headers to the server including `user agent: ` and receiving response headers including `Accept-CH: sec-ch-ua-platform`. ![Requests using only Accept-CH header - Browser Detect component for Blazor](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/acceptch.svg) During this initial request, the client will record the `Accept-CH` preferences and on subsequent requests include `sec-ch-ua-platform` by default. To further optimize this flow, the new `Critical-CH` header can be used in addition to the `Accept-CH` header to reissue the request header immediately, without the need for a page reload. The following diagram shows the browser sending request headers to the server including `user agent: ` and receiving response headers including `Accept-CH: sec-ch-ua-platform` and `Critical-CH: sec-ch-ua-platform`. The browser then sends request headers to the server immediately. ![Requests using Critical-CH and Accept-CH headers - Browser Detect component for Blazor](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/criticalch.svg) Starting with Microsoft Edge version 96, you can use the new `Critical-CH` header to receive desired high entropy headers with optimized performance. Remember that `Critical-CH` and `Accept-CH` preferences persist until session cookies are cleared, or until a user clears site data or cookies for a given origin. For more information about `Critical-CH`, refer to [Client Hint Reliability](https://github.com/WICG/client-hints-infrastructure/blob/main/reliability.md).[](https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11#detecting-specific-windows-versions) ## Detecting specific Windows versions To detect specific versions of Windows, use the following values for `platformVersion` in User-Agent Client Hints: Version`platformVersion`Win7/8/8.10Win10 15071Win10 15112Win10 16073Win10 17034Win10 17095Win10 18036Win10 18097Win10 19038Win10 19098Win10 200410Win10 20H210Win10 21H110Win10 21H210Win1113+## Using the component Now, we can start to use the browser detect component for Blazor. To detect in a correct way the correct version of the operating system and the architecture of the CPU, the component has to run few tests that take time. For this reason I added 2 events in the component: - WindowsArchitectureUpdate - WindowsVersionUpdate So, if you want to receive the notification when of the correct version of Windows and the CPU architecture, the component is like this code: ``` ``` then the functions look like ``` public BrowserInfo? Info { get; set; } public string? WindowsInfo { get; set; } = ""; public string? WindowsCPUInfoString { get; set; } private void WindowsArchitectureString(string cpu) { WindowsCPUInfoString = cpu; } private void WindowsUpdateString(string version) { WindowsInfo = version; } ``` Both events return a simple string with the values. For example: - if the operating system is `Windows 11`, the `WindowsUpdateString` receives the string `11`; - if the operating system is `Windows 10 1809`, the `WindowsUpdateString` receives the string `10 (1809)` - if the CPU is 32 bit, the `WindowsArchitectureString`, receives the string `x86` ##### Windows Architecture Values - x86\_64 - x86 - ARM64 - ARM32 ##### Windows Version VersionplatformVersionWin7/8/8.17/8/8.1Win10 150710 (1507)Win10 151110 (1511)Win10 160710 (1607)Win10 170310 (1703)Win10 170910 (1709)Win10 180310 (1803)Win10 180910 (1809)Win10 190310 (1903 or 10 1909)Win10 190910 (1903 or 10 1909)Win10 200410 (2004 or 20H2 or 21H1 or 21H2)Win10 20H210 (2004 or 20H2 or 21H1 or 21H2)Win10 21H110 (2004 or 20H2 or 21H1 or 21H2)Win10 21H210 (2004 or 20H2 or 21H1 or 21H2)Win1111## Wrap up In conclusion, I hope the Browser Detect component for Blazor can help in your projects. If you need help, here some links: - [Browser detect component for Blazor](https://puresourcecode.com/dotnet/blazor/browser-detect-component-for-blazor/) (first version) - [GitHub](https://github.com/erossini/BlazorBrowserDetect) repository - [NuGet](https://www.nuget.org/packages/PSC.Blazor.Components.BrowserDetect/) - [Try it now!](https://browserdetect.puresourcecode.com/) - [Forum](https://puresourcecode.com/forum/browser-detect-for-blazor/) **Categories:** .NET6, Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly --- ### [Play blackjack with Python](https://puresourcecode.com/programming-languages/python/play-blackjack-with-python/) **Published:** March 9, 2023 **Author:** Enrico **Excerpt:** In this new post, play blackjack with Python, I show how to create a simple application to play this popular game. **Content:** In this new post, play blackjack with [Python](https://puresourcecode.com/programming-languages/python/getting-started-with-python/), I show how to create a simple application to play this popular game. Blackjack, also known as 21, is a card game where players try to get as close to 21 points as possible without going over. This program uses images drawn with text characters, called ASCII art. American Standard Code for Information Interchange (ASCII) is a mapping of text characters to numeric codes that computers used before Unicode replaced it. ![Blackjack in action with Visual Studio Code - Play blackjack with Python](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/image-4.png?resize=640%2C344&ssl=1)Blackjack in action with Visual Studio Code ## Start the code First think to do is to import the module `sys` and `random`. I will use the `sys` to exit the `while` loop and `random` module to shuffle the desk of cards. ``` import sys, random ``` Then, I have to setup the constants for the deck using the `chr` function and assign to a variable the correspondent character. ## Set up the main function Then, I define the new constants for the suits and use them later in the code. ``` HEARTS = chr(9829) # Character 9829 is '♥'. DIAMONDS = chr(9830) # Character 9830 is '♦'. SPADES = chr(9824) # Character 9824 is '♠'. CLUBS = chr(9827) # Character 9827 is '♣'. BACKSIDE = 'backside' ``` Then, I’m going to define the main function printing same text and set up the money variable. ``` def main(): print('''Blackjack Rules: Try to get as close to 21 without going over. Kings, Queens, and Jacks are worth 10 points. Aces are worth 1 or 11 points. Cards 2 through 10 are worth their face value. (H)it to take another card. (S)tand to stop taking cards. On your first play, you can (D)ouble down to increase your bet but must hit exactly one more time before standing. In case of a tie, the bet is returned to the player. The dealer stops hitting at 17.''') money = 5000 ``` The application can start only if I call the `main` function. For this reason, I have to call the `main` function at the end of the application with this code ``` if __name__ == '__main__': main() ``` ## Shuffle the deck After that, I want to create a function that returns the deck: from 2 to 10 values and the face and aces cards for each suit. The `"""` is the comment to the function that it will show if you call the `help` function for this function. ``` def getDeck(): """Return a list of cards from the deck.""" deck = [] for suit in (HEARTS, DIAMONDS, SPADES, CLUBS): for rank in range(2, 11): # add the numbered cards deck.append((str(rank), suit)) for rank in ('J', 'Q', 'K', 'A'): # add the face and ace cards deck.append((rank, suit)) random.shuffle(deck) return deck ``` ## Getting the bet So, the next step it to write a function that takes the bet from the player. If the player texts “QUIT” the application will terminate invoking `sys.exit()`. `input` is waiting for the user to type a value. Then, the application will check if the value is a valid bet. If it is, the function will print out an error and wait for another input. If the value is accepted, the function will return the value of the bet. I like to point out that I use the function `isdecimal` to check if the value is a decimal number. Also, in the `if` I like to syntax to verify if the value is a decimal number between 1 and the `maxBet`. ``` def getBet(maxBet): """Ask the player how much money they want to bet for this round.""" while True: # keep asking until they enter a valid amount print('How much do you bet? (1-{}, or QUIT)'.format(maxBet)) bet = input('> ').upper().strip() if bet == 'QUIT': print('Thanks for playing!') sys.exit() if not bet.isdecimal(): # if the player didn't enter a number, ask again print('Please enter a number.\n') continue bet = int(bet) if 1 **Categories:** Python **Tags:** games, python **Hashtags:** python --- ### [Bitmap message in Python](https://puresourcecode.com/programming-languages/python/bitmap-message-in-python/) **Published:** March 8, 2023 **Author:** Enrico **Excerpt:** I continue to explore with a new example where I create a bitmap message in Python. Based on an string image, I change it with the user input **Content:** In this post, I continue to explore with a new example where I create a bitmap message in [Python](https://puresourcecode.com/programming-languages/python/getting-started-with-python/). So, this program uses a multiline string as a *bitmap*, a 2D image with only two possible colors for each pixel, to determine how it should display a message from the user. In this bitmap, space characters represent an empty space, and all other characters are replaced by characters in the user’s message. After that, the provided bitmap resembles a world map, but you can change this to any image you’d like. The binary simplicity of the space-or-message-characters system makes it good for beginners. Try experimenting with different messages to see what the results look like! ## The code Now, the code for the bitmap message in Python. Try to change the image I put in the program with another one that you like or copy the following code. ``` import sys # (!) Try changing this multiline string to any image you like # There are 68 periods along the top and bottom of this string: bitmap = """ .................................................................... ************** * *** ** * ****************************** ********************* ** ** * * ****************************** * ** ***************** ****************************** ************* ** * **** ** ************** * ********* ******* **************** * * ******** *************************** * * * **** *** *************** ****** ** * **** * *************** *** *** * ****** ************* ** ** * ******** ************* * ** *** ******** ******** * *** **** ********* ****** * **** ** * ** ********* ****** * * *** * * ****** ***** ** ***** * ***** **** * ******** ***** **** ********* **** ** ******* * *** * * ** * * ....................................................................""" print('Bitmap Message, by Al Sweigart al@inventwithpython.com') print('Enter the message to display with the bitmap.') message = input('> ') if message == '': sys.exit() # Loop over each line in the bitmap: for line in bitmap.splitlines(): # Loop over each character in the line: for i, bit in enumerate(line): if bit == ' ': # Print an empty space since there's a space in the bitmap: print(' ', end='') else: # Print a character from the message: print(message[i % len(message)], end='') print() # Print a newline. ``` ![The output in Visual Studio Code - Bitmap message in Python](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/image-3.png?resize=640%2C527&ssl=1)The output in Visual Studio Code ## How It Works So, a line of 68 periods at the top and bottom of the pattern acts as a ruler to help you align it correctly. However, the program will still work if you make typos in the pattern. The `bitmap.splitlines()` method call on line 43 returns a list of strings, each of which is a line in the multiline `bitmap` string. Using a multiline string makes the bitmap easier to edit into whatever pattern you like. The program fills in any non-space character in the pattern, which is why asterisks, periods, or any other character do the same thing. The `message[i % len(message)]` code on line 51 causes the repetition of the text in `message`. As `i` increases from `0` to a number larger than `len(message)`, the expression `i % len(message)` evaluates to `0` again. This causes `message[i % len(message)]` to repeat the characters in `message` as `i` increases. **Categories:** Python **Tags:** python **Hashtags:** python --- ### [Bagels a logic game in Python](https://puresourcecode.com/programming-languages/python/bagels-a-logic-game-in-python/) **Published:** March 7, 2023 **Author:** Enrico **Excerpt:** In Bagels, a deductive logic game in Python, you must guess a secret three-digit number based on clues.This is the first of a list of examples **Content:** In Bagels, a deductive logic game in [Python](https://puresourcecode.com/programming-languages/python/getting-started-with-python/), you must guess a secret three-digit number based on clues. The game offers one of the following gints in response to your guess: - Pico: when your guess has a correct digit in the wrong place - Fermi: when your guess has a correct digit in the correct place - Bagels if your guess has no correct digits. You have 10 tries to guess the secret number. ![Bagels in action with Visual Studio Code - Bagels a logic game in Python](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/image-1.png?resize=640%2C344&ssl=1)Bagels in action with Visual Studio Code ## How it works Keep in mind that this program uses not integer values but rather string values that contain numberic digits. For example, `426` is a different value than ‘426’. We need to do this because we are performing string comparison with the secret number, not math operations. Remember that `0` can be a leading digit: the string ‘026’ is different from ’26’, but the integer ‘026’ is the same as ’26’. ## The code ``` import random # (!) Try setting this to 1 or 10 NUM_DIGITS = 3 # (!) Try setting this to 1 or 10 MAX_GUESSES = 10 def main(): print('''Bagels, a deductive logic game. I am thinking of a {}-digit number with no repeated digits. Try to guess what it is. Here are some clues: When I say: That means: Pico One digit is correct but in the wrong position. Fermi One digit is correct and in the right position. Bagels No digit is correct. For example, if the secret number was 248 and your guess was 843, the clue would be Fermi Pico.'''.format(NUM_DIGITS)) # main game loop while True: # This stores the secret number the plaer needs to guess: secretNum = getSecretNum() print('I have thought up a number') print(' You have {} guesses to get it right'.format(MAX_GUESSES)) numGuesses = 1 while numGuesses MAX_GUESSES: print('You have run out of guesses') print('The answer was {}.'.format(secretNum)) # Ask player if they want to play again print('Do you want to play again? (yes or no)') if not input('> ').lower().startswith('y'): break print('Thanks for playing!') def getSecretNum(): """ Returns a string made up of NUM_DIGITS unique random digits. """ # create a list of digits 0 to 9 numbers = list('0123456789') # shuffle them into random order random.shuffle(numbers) # get the first NUM_DIGITS digits in the list for the secret number: secretNum = '' for i in range(NUM_DIGITS): secretNum += str(numbers[i]) return secretNum def getClues(guess, secretNum): """ Returns a string with the Pico, Fermi, Bagels clues for a guess and secret number pair """ if guess == secretNum: return 'You got it!' clues = [] for i in range(len(guess)): if guess[i] == secretNum[i]: # A correct digit is in the correct place clues.append('Fermi') elif guess[i] in secretNum: # A correct digit is in the wrong place clues.append('Pico') if(len(clues) == 0): # There are no correct answers return 'Bagels' else: # Sort the clues into alphabetical order so their original order # doesn't give information way clues.sort() # Make a single string from the list of string clues return ' '.join(clues) # if the program is run (instead of imported), run the game: if __name__ == '__main__': main() ``` ## The Help function Now, in code of Bagels a logic game in Python, you see in line 60 that after the function `getSecretNum` I added 3 quotation marks (`"""`). In [Python](https://puresourcecode.com/programming-languages/python/getting-started-with-python/), you can use triple quotation marks (“””) to create a `docstring` for a function, class, module, or method. A docstring is a string literal that documents what the code does, how to use it, what parameters it takes, what it returns, and any other information that might be useful for users or developers. This docstring follows the [PEP 257 conventions for docstring formatting](https://peps.python.org/pep-0257/). You can access the docstring of any Python object by using its `__doc__` attribute or the built-in `help()` function. So, in my code I added after the `main()` function a call to the `help` for the function `getSecretNum`. In this way, the program prints on screen the help for the function before continues with the main part. In the following screenshot, the result in Visua Studio Code. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/image-2.png?resize=640%2C697&ssl=1) **Categories:** Python **Tags:** python **Hashtags:** python --- ### [Hogwarts Legacy all house tokens locations](https://puresourcecode.com/games/hogwarts-legacy-all-house-tokens-locations/) **Published:** March 6, 2023 **Author:** Enrico **Excerpt:** Hogwarts Legacy all house tokens Locations, also known as Daedalian Keys. Finding all House Tokens is required to complete The Daedalian Keys **Content:** Hogwarts Legacy is [the best game](https://puresourcecode.com/games/hogwarts-legacy-is-a-successful-game/) released this here so far. I really enjoy playing with it and for this reason I’m creating few tutorials about the game (the previous was about the [magical cat](https://puresourcecode.com/games/how-to-find-the-magical-cat-in-hogwarts-legacy/)). On my [YouTube channel](https://www.youtube.com/channel/UC2jeteqpm3sUDqQpKGqpCLg?sub_confirmation=1), you can follow me in the game all the time. Hogwarts Legacy has 16 House Token Locations, also known as **Daedalian Keys**. Finding all House Tokens is required to complete **Side Quest: The Daedalian Keys** and to open the **House Chest** in your Dorm. This will unlock a unique outfit for your house which is one of the best in the game. Completing All Side Quests is also needed for The Good Samaritan trophy & achievement. None of the House Tokens are missable. You can still collect all of them after the story in free-roam. They only spawn after starting The Daedalian Keys side quest. All of them are within Hogwarts Castle. To collect the last House Token you must have finished **Main Quest House Token #25: The Caretaker’s Lunar Lament**, which unlocks the ability to open Level 1 Door Locks. So while this quest can be started very early, it is not possible to complete it until much later in the story. The way House Tokens work is that you must first find a flying key (“Daedalian Key”). This is always near the cabinet that contains the House Token. Then the key will automatically fly to the cabinet. You can follow the golden trail the key leaves behind to see the path to the cabinet. Interact with the cabinet and slap the key when it hovers over the lock. This opens it and you get 1 house token per cabinet. These are not marked on the map and that’s why they can be particularly tricky to find. Always use Revelio (D-Pad Right) to mark the key and cabinet in blue color, this makes them much easier to spot. It’s recommended to unlock all Floo Flames (Fast Travel Points) in Hogwarts first. We will use these as reference points for each token location. ## How to Start The Daedalian Keys Quest First, talk to Nellie Oggspire in Hogwarts -> The Astronomy Wing -> Transfiguration Courtyard. This is available after **Main Quest #7: Welcome to Hogsmeade**. The first House Token is part of the quest objective. Then you must find the remaining 15 yourself. ## All House Token Locations in Hogwarts Legacy ### House Token #1 – The Astronomy Wing > Astronomy Tower The first key is automatically marked by a quest icon on the map. It’s in the Astronomy Classroom, but first you need to grab the key. From Astronomy Tower fast travel point walk upstairs and the key will automatically start flying downstairs to the **Astronomy Classroom**. Use *Revelio* to mark it blue and interact with the cabinet. Then interact with the flying key at the cabinet and slap it into the lock. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-1-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-1-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-1-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-1-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-1-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-1-3.jpg?ssl=1)### House Token #2 – The Astronomy Wing > Defence Against the Dark Arts Classroom Fast travel to “**Defence Against the Dark Arts Classroom**”. From where you spawn, go down the stairs on the left. The key will be straight ahead, in front of the rhino skeleton. Go near it to make it fly up. Now go back upstairs where you came from, take the path left to find the cabinet (can use *Revelio* again to mark it). Outside the classroom on the balcony. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-2-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-2-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-2-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-2-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-2-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-2-3.jpg?ssl=1)### House Token #3 – The Bell Tower Wing > Bell Tower Courtyard Fast travel to “**Bell Tower Courtyard**”. From where you spawn, turn left and go up the first set of stairs, then go straight through the door. Behind the door is a staircase that keeps going up on the left. Keep following the staircase all the way to the top, along the way you will find the flying key and at the top is the cabinet. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-3-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-3-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-3-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-3-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-3-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-3-3.jpg?ssl=1)### House Token #4 – The Grand Staircase > Grand Staircase Tower Fast travel to “**Grand Staircase Tower**”. From where you spawn, turn right, then go down the stairs on the left. Keep going all the way down, along the way you will pick up the flying key (after going downstairs 2 times). The cabinet is found after going all the way down the staircase, just keep following the spiral staircase until it stops and the cabinet will be in front of you, next to a puzzle door, before the entrance to Great Hall. [![Hogwarts Legacy all house tokens Locations](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-4-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-4-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-4-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-4-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-4-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-4-3.jpg?ssl=1)### House Token #5 – The Grand Staircase > Quad Courtyard Fast travel to “**Quad Courtyard**”. From where you spawn there will be a dragon-fountain and staircase in front of you. Go up the set of stairs, then up the next stairs to the left. Up there you will see the flying key. Turn around and head to the west-end of this courtyard area (check the HUD minimap where it shows the “W” letter for West), and follow the golden trail for the flying key as it shows you the path. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-5-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-5-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-5-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-5-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-5-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-5-3.jpg?ssl=1)### House Token #6 – The Great Hall > Great Hall Fast travel to “**Great Hall**”. From where you spawn turn around and go through the two big exit gates of the Great Hall. Then go straight to go through the next big gate across from Great Hall. In this new room you find the key flying in the middle and the cabinet downstairs on the right. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-6-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-6-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-6-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-6-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-6-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-6-3.jpg?ssl=1)### House Token #7 – The Great Hall > Great Hall Fast travel to “**Great Hall**”. From where you spawn, turn left to find this flying key on the east-side of the Great Hall by the fireplace. As usual you can highlight it with *Revelio* and follow the trail of the key. The cabinet is on the right side of Great Hall. In the North-West corner of Great Hall go up the stairs to find it. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-7-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-7-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-7-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-7-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-7-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-7-3.jpg?ssl=1)### House Token #8 – The Library Annex > Potions Classroom Fast travel to “**Potions Classroom**”. From where you spawn, go through the door straight in front of you. Then turn left to see the flying key (as usual can cast Revelio to mark it blue). Now go down the stairs, go all the way to the very end to find the cabinet at the end of the stairs. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-8-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-8-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-8-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-8-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-8-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-8-3.jpg?ssl=1)### House Token #9 – The Library Annex > Central Hall Fast travel to “Central Hall”. From where you spawn, go down the stairs in front, then turn right, down the stairs, turn left to see the flying key. Go back up the last stairs and straight across to the other end of the room, go down the stairs there and turn right for the cabinet. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-9-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-9-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-9-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-9-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-9-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-9-3.jpg?ssl=1)### House Token #10 – The Library Annex > Central Hall Fast travel to “Central Hall”. From where you spawn, go up the stairs on the right, then the next 2 sets of stairs all the way to the top (can only go one way upstairs). At the top turn right to see another staircase but don’t go up, instead look behind the staircase to find the key. The cabinet is down the previous staircase on the right. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-10-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-10-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-10-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-10-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-10-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-10-3.jpg?ssl=1)### House Token #11 – The Library Annex > Library Fast travel to “Library”. From where you spawn, turn slightly left, cast Revelio to mark the flying key and check between the bookshelves. After finding it, go back to the fast travel point. When facing the fast travel point go right to find the cabinet (can cast Revelio again). [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-11-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-11-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-11-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-11-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-11-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-11-3.jpg?ssl=1)### House Token #12 – Secret Rooms > The Map Chamber Fast Travel to “The Map Chamber” (unlocked after Main Quest House Token #18: Jackdaw’s Rest). From where you spawn, turn around and go up the stairs all the way (there are two spiral staircases, go up the second spiral staircase too). At the top go through the metal door, to the left go downstairs and straight ahead to find this key. After going near it, turn around (in the direction you came from) and take the path left of the stairs to find the cabinet. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-12-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-12-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-12-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-12-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-12-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-12-3.jpg?ssl=1)### House Token #13 – Secret Rooms > The Map Chamber Go back to where you found the previous key for House Token #12. Follow that corridor to find a big sleeping dragon statue. Around it flies this key. Then head upstairs to find this cabinet straight in front of you after the 2nd set of stairs. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-13-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-13-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-13-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-13-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-13-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-13-3.jpg?ssl=1)### House Token #14 – The South Wing > Faculty Tower Fast travel to “Faculty Tower”. From where you spawn, go straight ahead and enter the first door on the right. The cabinet will be on the left side after going through the door (as usual can highlight it with Revelio). To get the key you must go up the stairs in front of you, it’s after going up two stairs, will float in front of the 2nd sets of stairs where a door puzzle is. Again, can cast Revelio to see it easily. It will float down and then you can open the cabinet. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-14-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-14-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-14-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-14-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-14-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-14-3.jpg?ssl=1)### House Token #15 – The South Wing > Hospital Wing Fast travel to “Hospital Wing”. From where you spawn, turn around and go down the spiral staircase. Then the cabinet will be to the left, the key will be in the corridor to the right. [![Hogwarts Legacy all house tokens Locations](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-15-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-15-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-15-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-15-2.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-15-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-15-3.jpg?ssl=1)### House Token #16 – The South Wing > Clock Tower Courtyard Requires Alohomora Spell to open Level 1 locks (Story Unlock from Main Quest House Token #25: The Caretaker’s Lunar Lament). Fast travel to “Clock Tower Courtyard”. From where you spawn go straight and look slightly right to find a door with a Level 1 Lock. Open this using Alohomora Spell and head upstairs into the Clock Tower. After going up the first stairs you can find the flying key on the left. Then keep going upstairs all the way to the top to find the cabinet. [![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-16-1.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-16-1.jpg?ssl=1)[![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-16-2.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-16-2.jpg?ssl=1)[![Hogwarts Legacy all house tokens Locations](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-16-3.jpg?resize=640%2C360&ssl=1)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/03/hogwarts-legacy-house-token-location-16-3.jpg?ssl=1) **Categories:** Games **Tags:** games, hogwarts-legacy **Hashtags:** hogwarts-legacy --- ### [Getting started with Python](https://puresourcecode.com/programming-languages/python/getting-started-with-python/) **Published:** March 6, 2023 **Author:** Enrico **Excerpt:** Python is a high-level, general-purpose and a very popular programming language that lets you work quickly and integrate systems effectively **Content:** Following my previous article where I explored [Java](https://puresourcecode.com/programming-languages/java/getting-started-with-java/), in this new post I like to start to explore Python, another programming language, very popular and at the top of the list of languages that developers love. In the Stack overflow survey, Python is the 4th language used from professional developer and the first in the TIOBE Index. ![Stackoverflow survey - Getting started with Python](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-1.png?resize=640%2C303&ssl=1)Stackoverflow survey [Python](https://docs.python.org/3/tutorial/index.html) is a high-level, general-purpose and a very popular programming language that lets you work quickly and integrate systems more effectively. Python allows programming in Object-Oriented and Procedural paradigms. Python was created in the early 1990s by Guido van Rossum at Stichting Mathematisch Centrum in the Netherlands as a successor of a language called ABC. ## Advantages and disadvantages Python has many advantages and disadvantages as a programming language. Some of the advantages are: - **Easy to use and learn**: Python has a simple and expressive syntax that is similar to the English language. It does not require semicolons or braces and uses indentation to define blocks of code. - **Free and open source**: Python is developed under an OSI-approved open source license that allows it to be used and distributed freely, including for commercial purposes. - **Portable**: Python can run on various platforms, such as Windows, Linux, Mac OS, etc. without requiring any changes in the source code. - **Extensive third-party libraries**: Python has a large and diverse collection of libraries that provide various functionalities, such as web development, data analysis, machine learning, etc. - **Object-oriented and dynamic**: Python supports multiple programming paradigms, such as object-oriented, procedural, functional, etc. Python is also dynamically typed, which means that the type of variables is determined at runtime. Some of the disadvantages are: - **Slow performance**: Python is an interpreted language that is dynamically typed, which makes it slower than compiled and statically typed languages, such as Java or C. - **Distinct nomenclature**: Python has some features that are different from other languages, such as the use of underscores, self keyword, etc. This can make it confusing for beginners or programmers who are used to other languages. - **Code can become unruly in size**: Python does not have strict rules for code organization, which can lead to large and messy code bases if not managed properly. - **Global Interpreter Lock (GIL) and threading limitations**: Python has a mechanism called GIL that prevents multiple threads from executing Python code at the same time, which limits the concurrency and parallelism of Python programs. - **Runtime errors**: Python does not check for errors at compile time, which means that some errors may only be detected at runtime, which can cause unexpected crashes or bugs. ## How popular is Python? Python is a very popular programming language in 2023. According to the [TIOBE Index](https://www.tiobe.com/tiobe-index/), which measures the popularity of programming languages based on the number of skilled engineers, courses, and third-party vendors worldwide, Python ranked **first** in August 2022 with a **15.42%** share. Python has been consistently growing in popularity over the years, and has surpassed languages such as [Java](https://puresourcecode.com/programming-languages/java/getting-started-with-java/), C, and [C#](https://puresourcecode.com/category/dotnet/csharp/). Some of the reasons why Python is so popular are: - It’s one of the best languages when learning to code: Python has a simple and expressive syntax that is easy to understand and write. It also has a large and supportive community that provides many resources and tutorials for beginners. - It is heavily used in the **Internet of Things**: Python is suitable for developing applications for small and low-power devices, such as Raspberry Pi, Arduino, etc. Python also has libraries that support communication protocols, such as MQTT, CoAP, etc. - It is instrumental in data science and AI: Python is widely used for data analysis, visualization, machine learning, deep learning, natural language processing, etc. Python has many libraries and frameworks that provide these functionalities, such as NumPy, pandas, scikit-learn, TensorFlow, PyTorch, etc. ## Wrap up In conclusion, with this post “Getting started with Python”, I like to look around and learn more about this programming language. I have some new code to publish very soon. Stay tuned! **Categories:** Python **Tags:** programming-languages, python **Hashtags:** python --- ### [How to find the magical cat in Hogwarts Legacy](https://puresourcecode.com/tips-tricks/how-to-find-the-magical-cat-in-hogwarts-legacy/) **Published:** March 4, 2023 **Author:** Enrico **Excerpt:** Kneazles won't appear in the wild until players have completed the side quest titled, "The Elf, the Nab-sack, and the Loom" **Content:** [Hogwarts Legacy](https://puresourcecode.com/games/hogwarts-legacy-is-a-successful-game/) is a very successful game: in 2 weeks, [Warner Bros](https://twitter.com/HogwartsLegacy) sells over 12 million units. And because it is popular and like the idea to live in Harry Potter’s shoes, I decided to play with this game for [Xbox](https://puresourcecode.com/tag/xbox/) and I really enjoy it. I don’t have many posts about [games](https://puresourcecode.com/category/games/), but I think it is time to start to write something about it. So, in between taking classes and diving into various dungeons during your Hogwarts Legacy playthrough, you can spend your time running into various magical beast dens and capturing these fantastic creatures. One of the 13 magical beast species is the Kneazle, a small mammalian creature that looks very much like a large housecat. They’re relatively easy to catch compared to other creatures as long as you know where to find them. ## Hogwarts Legacy: how to unlock Kneazle First, Kneazles won’t appear in the wild until players have completed the side quest titled, “**The Elf, the Nab-sack, and the Loom**“, This quest is given out by Deek the House-Elf in **The Room of Requirement** and can be accessed relatively early on in [Hogwarts Legacy](https://puresourcecode.com/games/hogwarts-legacy-is-a-successful-game/). You’ll want to take time to complete all of the early mainline quests relatively quickly before exploring as this unlocks more things for you to do. ## What makes a Kneazle magical and not just a housecat? Now, while Kneazles very much resemble housecats they are inherently magical. In the wizarding world, their whiskers can even be used to create less powerful wands than those embued with a **Phoenix Feather**, **Unicorn Hair**, or **Dragon Heartstring**. Additionally, these cats also have a keen sense for telling if someone is untrustworthy and can be highly aggressive. However, if a Kneazle determines that a person is trustworthy and loyal it can bond with that individual and help protect them. If you’ve read the Harry Potter books, you’ll know that a Kneazle named Crookshanks takes a prominent role in the third book: **The Prisoner of Azkaban**. ## Hogwarts Legacy: Kneazle Den locations **Den Location 1:** Go to World Map → Hogwarts Valley → Brocburrow Floo Flame and head south. **Den Location 2:** Go to World Map → South Sea Bog → Northern South Sea Bog Floo Flame and head south until you get beyond the marshes. **Den Location 3:** Go to World Map → Marunweem Lake → Marunweem Ruins Floo Flame and head south along the path to the small beach across the bridge. ## Hogwarts Legacy: how to catch a Kneazle **Step 1:** Without getting too close to the Kneazles, use **Disillusionment** to turn invisible then get within a few feet of a Kneazle. **Step 2:** Cast **Levioso** to lift the Kneazle into the air. If you’re somewhat far from the magical cat then take a moment to run closer and then cast Levioso again to keep it floating. **Step 3:** Shortly after casting Levioso, pull out the Nab-sack and press the button that shows up on your screen multiple times to capture this magical beast. ## Magical meow Although they don’t look like they should be considered magical beasts at first glance, Kneazles are very perceptive creatures naturally imbued with magic. There are plenty of Kneazle Dens throughout the Hogwarts Legacy World Map. As long as you take the time to travel to these locations and whip out your Nab-sack at the right time, you’ll be able to collect plenty of these creatures very quickly. [Tweets by HogwartsLegacy](https://twitter.com/HogwartsLegacy?ref_src=twsrc%5Etfw) **Categories:** Games, Tips & tricks **Tags:** games, hogwarts-legacy --- ### [Autocomplete component for Blazor](https://puresourcecode.com/dotnet/net-core/autocomplete-component-for-blazor/) **Published:** February 28, 2023 **Author:** Enrico **Excerpt:** The Autocomplete for Blazor component offers simple and flexible autocomplete type-ahead functionality for Blazor WebAssembly and Server **Content:** The Autocomplete component for Blazor offers simple and flexible autocomplete type-ahead functionality for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). The components is build with [NET6](https://puresourcecode.com/tag/net6/). If you need support for this component or you have a suggestion or comment, please use my [Forum](https://puresourcecode.com/forum/autocomplete-blazor/). ![Autocomplete component for Blazor in action](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/221885675-c3fc3146-ac13-4fe6-8305-f6d4f396c8bd.gif?w=640&ssl=1)Autocomplete component for Blazor in action ## Usage ### 1. Install You can install from NuGet using the following command: `Install-Package PSC.Blazor.Components.AutoComplete` Or via the Visual Studio package manger. ## 2. Add using to your project Blazor Server applications will need to include the following CSS and JS files in their `_Host.cshtml`. Alternative, Blazor Client applications will need to include the following CSS and JS files in their `Index.html` . In the `head` tag add the following CSS. ``` ``` Then add the JS script at the bottom of the page using the following script tag. ``` ``` I would also suggest adding the following using statement to your main `_Imports.razor` to make referencing the component a bit easier. ``` @using PSC.Blazor.Components.AutoComplete ``` ## Options The Autocomplete component for Blazor can be used standalone or as part of a form. When used in a form the control fully integrates with Blazor’s forms and authentication system. Below is a list of all the options available on the AutoComplete. ### Templates - `ResultTemplate` (Required) – Allows the user to define a template for a result in the results list - `SelectedTemplate` (Required) – Allows the user to define a template for a selected item - `HelpTemplate` – Allows the user to define a template to show when the `MinimumLength` to perform a search hasn’t been reached - `NotFoundTemplate` – Allows the user to define a template when no items are found - `FooterTemplate` – Allows the user to define a template which is displayed at the end of the results list ### Parameters - `MinimumLength` (Optional – Default: 1) – Minimum number of characters before starting a search - `Debounce` (Optional – Default: 300) – Time to wait after last keypress before starting a search - `MaximumSuggestions` (Optional – Default: 10) – Controls the amount of suggestions which are shown - `Disabled` (Optional – Default: `false`) – Marks the control as disabled and stops any interaction - `EnableDropDown` (Optional – Default: `false`) – Allows the control to behave as a dropdown - `DisableClear` (Optional – Default : `false`) – Hides the clear button from the AutoComplete. Users can still change the selection by clicking on the current selection and typing however, they can’t clear the control entirely.’ - `ShowDropDownOnFocus` (Optional – Default: `false`) – When enabled, will show the suggestions dropdown automatically when the control is in search mode. If the control has a current value then the user would need to press the enter key first to enter search mode. - `StopPropagation` (Optional – Default: `false`) – Control the StopPropagation behavior of the input of this component. See this [Microsoft document](https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1#stop-event-propagation) - `PreventDefault` (Optional – Default: `false`) – Control the PreventDefault behavior of the input of this component. See this [Microsoft document](https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1#prevent-default-actions) ### More details The control also requires a `SearchMethod` to be provided with the following signature `Task(string searchText)`. The control will invoke this method passing the text the user has typed into the control. You can then query your data source and return the result as an `IEnumerable` for the control to render. If you wish to bind the result of the selection in the control to a different type than the type used in the search this is also possible. For example, if you passed in a list of `Person` but when a `Person` was selected you wanted the control to bind to an `int` value which might be the `Id` of the selected `Person`, you can achieve this by providing a `ConvertMethod` The convert method will be invoked by the control when a selection is made and will be passed the type selected. The method will need to handle the conversion and return the new type. If you want to allow adding an item based on the search when no items have been found, you can achieve this by providing the `AddItemOnEmptyResultMethod` as a parameter. This method will make the `NotFoundTemplate` selectable the same way a item would normally be, and will be invoked when the user selects the `NotFoundTemplate`. This method passes the `SearchText` and expects a new item to be returned. ### Local Data Example ``` @context.Title @context.Title (@context.Year) @code { [Parameter] protected IEnumerable Films { get; set; } private async Task SearchFilms(string searchText) { return await Task.FromResult(Films.Where( x => x.Title.ToLower().Contains(searchText.ToLower())).ToList()); } } ``` In the example above, the component is setup with the minimum requirements. You must provide a method which has the following signature `Task **Categories:** .NET Core **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly, type-ahead **Hashtags:** blazor, blazor-component --- ### [Earn Microsoft Rewards by using Bing](https://puresourcecode.com/news/earn-microsoft-rewards-by-using-bing/) **Published:** March 1, 2023 **Author:** Enrico **Excerpt:** After the release of the new Microsoft Bing with ChatGPT, users start to ask how to earn Microsoft Rewards point by using the new Bing **Content:** After the release of the new [Microsoft Bing with ChatGPT](https://puresourcecode.com/news/how-to-use-bing-with-chatgpt/), users start to ask how to earn Microsoft Rewards point by using the new Bing. Unfortunately, the answer is that you cannot earn Microsoft Rewards points by using the chat functionality within the new Bing. You can, however, continue to earn Microsoft Rewards points by using the standard search interface of Bing. ## What are Microsoft Rewards points? Microsoft Rewards points can be exchanged for gift cards, movies, games, nonprofit donations, and more. They used to be known as Bing Rewards points, but the name changed back in 2016. You earn points by using Microsoft services when signed in to your Microsoft account. Microsoft outlines the main ways you can earn points in a support document: - Search with Bing (level up faster by searching with Bing on Microsoft Edge). - Search the web through the search box on the taskbar on your Windows10 device. - Buy stuff from Microsoft Store online (from your mobile device, on Xbox One, in the Microsoft Store app on your Windows 10/11 or Windows 8.1 device, or via the web). For more info, see Shop and earn Microsoft Rewards points. - Use Cortana to search with Bing. - Explore the earn page and the points breakdown page. Opportunities to earn points, like taking quizzes or playing trivia games, are updated daily, so check in often! - Play selected games or complete selected quests on Xbox One. Launch the Rewards app to get started. For more info, see Earn rewards on Xbox. One of the easiest ways to earn Microsoft Rewards points is to use Bing as your search engine. There are [different levels](https://support.microsoft.com/en-us/topic/about-microsoft-rewards-status-levels-6ca5db8e-1e59-caa3-7d96-f7a1d5270c15) for earning rewards. A single search always earns you three Microsoft Rewards points, but the daily cap for how many points you can earn depends on your level. Microsoft announced a new version of [Bing in February 2023](https://puresourcecode.com/news/how-to-use-bing-with-chatgpt/). The revamped search engine uses artificial intelligence, ChatGPT, and Microsoft’s own proprietary tech to generate responses based on real-time information. There are two ways to interact with the new Bing.[](https://account.microsoft.com/privacy/ad-settings) *Note: The new Bing is currently in preview. You have to add your name to the wait list to try out the new Bing.* First, you can use the new Bing in a similar way to the old Bing (or any other popular search engine like Google). Go to [bing.com](https://www.bing.com/) and enter your query into the search box. You’ll then see search results. Every time you search for something using this method, you’ll earn three Microsoft Rewards points. There are caps for how many points you can earn each day on your phone or PC. If you use Bing as your default browser on all of your devices, you can rack up points without much thought or effort. If you click on the chat tab within Bing, you’ll open a new way to communicate with the search engine. Chat functionality allows you to make more complex inquiries and ask follow-up questions. You may think that a system designed to answer several questions within a thread would be a quick way to earn Microsoft Rewards points. That is not the case. Individual questions within a chat session do not earn you points. In fact, **using the chat functionality of the new Bing does not earn Microsoft Rewards points**. We reached out to Microsoft regarding Microsoft Rewards points. The company confirmed that chat sessions within the chat tab do not count toward earning Microsoft Rewards points but that normal searches still earn points. **Categories:** Microsoft, News, Tips & tricks **Tags:** bing, microsoft, rewards **Hashtags:** microsoft --- ### [Hogwarts Legacy is a successful game](https://puresourcecode.com/news/hogwarts-legacy-is-a-successful-game/) **Published:** February 27, 2023 **Author:** Enrico **Excerpt:** Hogwarts Legacy is a successful video game set in the Harry Potter world. It was released on February 3, 2023 **Content:** Hogwarts Legacy is a successful video game set in the Harry Potter world. It was released on February 3, 2023. The game has received mostly positive reviews from critics and fans, but some have criticized its open-world design and lack of originality. It is not the most played game at the moment, but it is among the top-selling games of 2023. Hogwarts Legacy is a video game that is set in the world of Harry Potter, but in the 1800s. It is an immersive, open-world action RPG where you can create your own character and explore Hogwarts and other locations12. You can also learn spells, potions, and talents, and uncover a secret that could threaten the wizarding world. People have different opinions about Hogwarts Legacy. Some are excited to play it and experience a new story in the Harry Potter universe. Some are concerned about the involvement of J.K. Rowling, who has been criticized for her views on transgender issues. Some are also worried about the game’s quality and delays. ## The story The story of Hogwarts Legacy is set in the 1890s, a century before Harry Potter’s time. You play as a student who has a mysterious power to access ancient magic. You will have to choose between using your power for good or evil, and face various threats from dark wizards, beasts, and secrets. The game features many original characters that you can interact with, such as your friends, professors, rivals, and enemies. Some of them are Amit Thakkar, a Ravenclaw student who wants to be famous; Professor Eulalie Hicks, a Charms teacher who helps you with your power; and Livia Blackthorn, a dark witch who leads a cult called The Unbound. The game also includes some familiar characters from the Harry Potter lore, such as Albus Dumbledore (a young teacher), Nicolas Flamel (the alchemist), and Bathilda Bagshot (the historian). ![Hogwarts Legacy - Hogwarts Legacy is a successful game](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/hogwarts_legacy_wallpaper.jpg?resize=640%2C360&ssl=1)Hogwarts Legacy ## The numbers Hogwarts Legacy fans have collectively put 267 million hours into the magical RPG since it was released just under three weeks ago. As revealed by [Variety](https://variety.com/2023/gaming/news/hogwarts-legacy-launch-numbers-harry-potter-1235530632/), Hogwarts Legacy’s popularity is continuing to soar as its players have been putting 23 million hours a day into the game, weeks after its full release. To put that into context a bit more, as of February 16, Variety reported that 152 million hours had been played, but by February 21 it had jumped up to that 267 million mark. That’s 115 million hours in just five days, or a community total of 23 million hours played every 24 real-world hours. Variety also reports that Hogwarts Legacy encouraged the highest traffic in the Harry Potter fan community, at least on the website Fandom, in more than seven years. Previously, the second Fantastic Beasts movie generated a similar kind of buzz, but this has since been eclipsed by Avalanche Software’s game. According to the article, Hogwarts Legacy-driven page views on the website surpassed the Fantastic Beasts heyday by 39% in just five days. ## Big money Warner Bros. Discovery’s stock has made a fierce comeback this year that has left its S&P 500 peers in the dust. The media giant’s share price has surged about 64% so far in 2023 after falling for the last three years, making it the best year-to-date performer among S&P 500 constituents. It’s beaten popular stocks including Tesla, Meta and Amazon, which have risen 60%, 38% and 11% respectively since the start of January. The strong rebound appears to be powered by the massive success of the highly anticipated Harry Potter-inspired video game, “Hogwarts Legacy.” The company announced last Thursday that “Hogwarts Legacy” has already sold 12 million units so far, reeling $850 million in sales, in the first two weeks since its launch. The game was already a top seller ahead of its release, in presales, beating other games like “Call of Duty: Modern Warfare II” and “Destiny 2,” even as it faced backlash and calls to boycott it due to Harry Potter author J.K. Rowling’s allegedly transphobic comments. CEO David Zaslav said his company’s hit video game business is now a “core part of our overall strategy.” “Hogwarts Legacy” is an action role-playing game set in the Harry Potter universe during the 1800s, a century before the events of the novels – and the player will play as a student at Hogwarts during that time. At close on Friday, Warner Bros stock stood at $15.55 per share. It fell on the day after the company posted fourth-quarter revenue results that missed analysts’ estimates. ![Hogwarts Legacy: the castle](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/hogwarts_legacy_castle.jpg?resize=640%2C360&ssl=1)Hogwarts Legacy: the castle ## My personal review So, I have to say that I really enjoy to play with Hogwarts Legacy and I think it is a successful game. The game drives you to learn step-by-step all the spells and try each of them. The graphic is very details and it seems quite real. Sometimes, the game doesn’t render very well some characters. Now, I play on [Xbox](https://puresourcecode.com/?s=xbox) [Series S](https://puresourcecode.com/tag/xbox-series-s/) and the game is very fluid and easy to understand. You must have 80Gb of space to install the game. I took a while to understand that when the your character has a gold circle/crown in front of him you have to protect him with the *Protego* spell. ![Here you have to use Protego](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-18.png?resize=640%2C366&ssl=1)Here you have to use *Protego* ## Wrap up In conclusion, Hogwarts Legacy is a successful game and I like to share with you more details about this game. I’m planning to write more posts with tips and tricks. You can follow me on [Twitch](https://www.twitch.tv/erossiniuk) where I share all my games. **Categories:** Games, News, Other **Tags:** happy-potter, hogwarts-legacy, xbox **Hashtags:** game, happy-potter, xbox --- ### [Timeline component for Blazor](https://puresourcecode.com/dotnet/blazor/timeline-component-for-blazor/) **Published:** February 22, 2023 **Author:** Enrico **Excerpt:** I introduce my new #Timeline component for #Blazor #WebAssembly and Blazor #Server. The components is build with #NET6. **Content:** In this new post, I introduce my new Timeline component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). The components is build with [NET6](https://puresourcecode.com/tag/net6/). This new component is very easy to use and customize. Here all the details. ![Example of Timeline - Timeline component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-12.png?resize=590%2C1024&ssl=1)Example of Timeline ## Usage ### 1. Install This component is a Nuget package available from [this link](https://www.nuget.org/packages/PSC.Blazor.Components.Timeline/). ### 2. Add using to your project. ``` @using PSC.Blazor.Components.Timeline @using PSC.Blazor.Components.Timeline.Enums ``` ### 3. Example code ``` @using PSC.Blazor.Components.Timeline Here you can write some text. The TimelineItem has the property for adding a button called ButtonText and you can add a link with Link. TimelineItem can be altered to appear on the right! Also, you can use the default icon or use one of the embedded icons with Icon. You can add images or any other HTML code Now, visit the page with the documentation and same examples. Use the forum to send your comment or submit your questions. ``` ## Anatomy of Timeline First, have a look of a simple timeline with 2 events. A `Timeline` is the container for a list of event and is responsible to render the UI. In the `Timeline` we can define the basic colours for the entire timeline. The timeline is responsive and tested with different browsers, devices and screen sizes. Also, I created tests using bUnit, the framework for testing Blazor. A `TimelineItem` is an event to display. Each `TimelineItem` has a title, can have a date, an icon (by default the icon is a calendar but you can use one of the other embedded icons or use yours in SVG format) and add the HTML code to display. Also, the `TimelineItem` can display a button with a link and change the colours for the title and the body. Plus, it is possible to choose the side of the event (`Left` of `Right`) for big screens. ![Anatomy of Timeline - Timeline component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-13.png?resize=640%2C567&ssl=1)Anatomy of Timeline In a mobile device or a table, the timeline is displayed as the following screenshot. The icon on the left and the event on the right for all item. ![Responsive timeline](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-14.png?resize=345%2C856&ssl=1)Responsive timeline ## Timeline properties and methods So, the timeline has the main component `Timeline` and then each element called `TimelineItem` that defines each event in time. ### Timeline properties NameDescription![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)ChildContentGets or sets the content of the child.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)ItemPositionOptionShould items be altered automatically or manual.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TextBgColorDescription background color.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TextColorDescription text color.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TitleTimeline title which will be displayed above.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TitleBgColorBackground color for title section.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TitleColorText color for title section.### Timeline item properties NameDescription![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)ButtonTextGets or sets the button text.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)ChildContentGets or sets the content of the child.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)IconGets or sets the icon.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)IconContentGets or sets the content of the icon.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)LinkGets or sets the link.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)PositionGets or sets the position.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TextBackgroundColorGets or sets the color of the text background.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TimeGets or sets the time.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TimelineGets or sets the timeline.![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TitleTitle for timeline item![image](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/220409836-c7035379-3fb1-48f2-9906-5c799b62e18e.png?w=640&ssl=1)TitleBackgroundColorGets or sets the color of the back ground.### Timeline Icon embedded - Clock - ClockDateTime - Default - DigitalNumber0 - DigitalNumber1 - DigitalNumber2 - DigitalNumber3 - DigitalNumber4 - DigitalNumber5 - DigitalNumber6 - DigitalNumber7 - DigitalNumber8 - DigitalNumber9 - HourGlass - HourGlassOld - ReminderClock - SandClock - Timeline - UserTime ## Wrap up In conclusion, the `Timeline` component for Blazor is here. If you need support and you have any comment, please use my [forum](https://puresourcecode.com/forum/timeline/). Also, you can see (soon) the full source code on [GitHub](https://github.com/erossini/BlazorTimeline). **Categories:** Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly --- ### [Testing Blazor components with bUnit](https://puresourcecode.com/dotnet/csharp/testing-blazor-components-with-bunit/) **Published:** February 23, 2023 **Author:** Enrico **Excerpt:** bUnit is a testing library for Blazor Components. Its goal is to make it easy to write comprehensive, stable unit tests **Content:** In this new post, I show a new framework for testing Blazor components called [bUnit](https://bunit.dev/index.html). *bUnit* is a testing library for Blazor Components. Its goal is to make it easy to write *comprehensive, stable* unit tests. With bUnit, you can: - Setup and define components under tests using C# or Razor syntax - Verify outcomes using semantic HTML comparer - Interact with and inspect components as well as trigger event handlers - Pass parameters, cascading values and inject services into components under test - Mock `IJSRuntime`, Blazor authentication and authorization, and others bUnit builds on top of existing unit testing frameworks such as xUnit, NUnit, and MSTest, which run the Blazor components tests in just the same way as any normal unit test. bUnit runs a test in milliseconds, compared to browser-based UI tests which usually take seconds to run. ## Create the first test First, we have to create a new test project using one of the available frameworks: - `xunit` – [xUnit](https://xunit.net/) - `nunit` – [NUnit](https://nunit.org/) - `mstest` – [MSTest](https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest) and after that, add bUnit to your test project. ## Writing tests for Blazor components So, testing Blazor components is a little different from testing regular C# classes: Blazor components are *rendered*, they have the *Blazor component life cycle* during which we can *provide input* to them, and they can *produce output*. Use **bUnit** to render the component under test, pass in its parameters, inject required services, and access the rendered component instance and the markup it has produced. Rendering a component happens through bUnit’s [TestContext](https://bunit.dev/api/Bunit.TestContext.html). The result of the rendering is an `IRenderedComponent`, referred to as a “rendered component”, that provides access to the component instance and the markup produced by the component. For example, in your Blazor application create a new file `HelloWorld.razor` with this content ``` Hello world from Blazor ``` This is a very basic component but we have the chance to render this component and test it. So, in your test class add the following code. ### xUnit ``` using Xunit; using Bunit; namespace Bunit.Tests { public class HelloWorldTest { [Fact] public void HelloWorldComponentRendersCorrectly() { // Arrange using var ctx = new TestContext(); // Act var cut = ctx.RenderComponent(); // Assert cut.MarkupMatches("Hello world from Blazor"); } } } ``` ### nUnit ``` using Bunit; using NUnit.Framework; namespace Bunit.Tests { public class HelloWorldTest { [Test] public void HelloWorldComponentRendersCorrectly() { // Arrange using var ctx = new Bunit.TestContext(); // Act var cut = ctx.RenderComponent(); // Assert cut.MarkupMatches("Hello world from Blazor"); } } } ``` ### MSTest ``` using Bunit; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Bunit.Tests { [TestClass] public class HelloWorldTest { [TestMethod] public void HelloWorldComponentRendersCorrectly() { // Arrange using var ctx = new Bunit.TestContext(); // Act var cut = ctx.RenderComponent(); // Assert cut.MarkupMatches("Hello world from Blazor"); } } } ``` ### Code explained The test above does the following: 1. Creates a new instance of the disposable bUnit [TestContext](https://bunit.dev/api/Bunit.TestContext.html), and assigns it to the `ctx` variable using the `using var` syntax to avoid unnecessary source code indention. 2. Renders the `` component using [TestContext](https://bunit.dev/api/Bunit.TestContext.html), which is done through the [RenderComponent(Action>)](https://bunit.dev/api/Bunit.TestContext.html#Bunit_TestContext_RenderComponent__1_Action_Bunit_ComponentParameterCollectionBuilder___0___) method. We cover passing parameters to components on the [Passing parameters to components](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html) page. 3. Verifies the rendered markup from the `` component using the `MarkupMatches` method. The `MarkupMatches` method performs a semantic comparison of the expected markup with the rendered markup. `TestContext` is an ambiguous reference – it could mean `Bunit.TestContext` or `Microsoft.VisualStudio.TestTools.UnitTesting.TestContext` – so you have to specify the `Bunit` namespace when referencing `TestContext` to resolve the ambiguity for the compiler. Alternatively, you can give bUnit’s `TestContext` a different name during import, e.g.: `using BunitTestContext = Bunit.TestContext;` ## Passing parameters to components bUnit comes with a number of ways to pass parameters to components under test: 1. In tests written in `.razor` files, passing parameters is most easily done with inside an inline Razor template passed to the `Render` method, although the parameter passing option available in tests written in C# files is also available here. 2. In tests written in `.cs` files, bUnit includes a strongly typed builder. There are two methods in bUnit that allow passing parameters in C#-based test code: - `RenderComponent` method on the test context, which is used to render a component initially. - `SetParametersAndRender` method on a rendered component, which is used to pass new parameters to an already rendered component. In the following sub sections, we will show both `.cs`– and `.razor`-based test code; just click between them using the tabs. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#regular-parameters)Regular parameters A regular parameter is one that is declared using the `[Parameter]` attribute. The following subsections will cover both *non*-Blazor type parameters, e.g. `int` and `List`, and the special Blazor types like `EventCallback` and `RenderFragment`. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#non-blazor-type-parameters)Non-Blazor type parameters Let’s look at an example of passing parameters that takes types which are *not* special to Blazor, i.e.: ``` public class NonBlazorTypesParams : ComponentBase { [Parameter] public int Numbers { get; set; } [Parameter] public List Lines { get; set; } } ``` This can be done like this: ``` public class NonBlazorTypesParamsTest { [Fact] public void Test() { using var ctx = new TestContext(); var lines = new List { "Hello", "World" }; var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Numbers, 42) .Add(p => p.Lines, lines) ); } } ``` The example uses the [ComponentParameterCollectionBuilder](https://bunit.dev/api/Bunit.ComponentParameterCollectionBuilder-1.html)‘s `Add` method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type for the value. This makes the builder’s methods strongly typed and refactor-safe. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#eventcallback-parameters)EventCallback parameters This example will pass parameters to the following two `EventCallback` parameters: ``` public class EventCallbackParams : ComponentBase { [Parameter] public EventCallback OnClick { get; set; } [Parameter] public EventCallback OnSomething { get; set; } } ``` This can be done like this: ``` public class EventCallbackParamsTest { [Fact] public void Test() { using var ctx = new TestContext(); Action onClickHandler = _ => { }; Action onSomethingHandler = () => { }; var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.OnClick, onClickHandler) .Add(p => p.OnSomething, onSomethingHandler) ); }` } } ``` The example uses the ComponentParameterCollectionBuilder’s `Add` method, which takes a parameter selector expression that selects the parameter using a lambda, and forces you to provide the correct type of callback method. This makes the builder’s methods strongly typed and refactor-safe. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#childcontent-parameters)ChildContent parameters The `ChildContent` parameter in Blazor is represented by a `RenderFragment`. In Blazor, this can be regular HTML markup, it can be Razor markup, e.g. other component declarations, or a mix of the two. If it is another component, then that component can also receive child content, and so forth. The following subsections have different examples of child content being passed to the following component: ``` public class ChildContentParams : ComponentBase { [Parameter] public RenderFragment ChildContent { get; set; } } ``` #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-html-to-the-childcontent-parameter)Passing HTML to the ChildContent parameter ``` public class ChildContentParams1Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .AddChildContent("Hello World") ); } } ``` The example uses the `ComponentParameterCollectionBuilder`‘s `AddChildContent` method to pass an HTML markup string as the input to the `ChildContent` parameter. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-component-without-parameters-to-the-childcontent-parameter)Passing a component without parameters to the ChildContent parameter To pass a component, e.g. the classic `` component, which does not take any parameters itself, to a `ChildContent` parameter, do the following: ``` public class ChildContentParams2Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .AddChildContent() ); } } ``` The example uses the `ComponentParameterCollectionBuilder`‘s `AddChildContent` method, where `TChildComponent` is the (child) component that should be passed to the component under test’s `ChildContent` parameter. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-component-with-parameters-to-the-childcontent-parameter)Passing a component with parameters to the ChildContent parameter To pass a component with parameters to a component under test, e.g. the `` component with the following parameters, do the following: ``` [Parameter] public string Heading { get; set; } [Parameter] public AlertType Type { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } ``` ``` public class ChildContentParams3Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .AddChildContent(alertParameters => alertParameters .Add(p => p.Heading, "Alert heading") .Add(p => p.Type, AlertType.Warning) .AddChildContent("Hello World") ) ); } } ``` The example uses the `ComponentParameterCollectionBuilder`‘s `AddChildContent` method, where `TChildComponent` is the (child) component that should be passed to the component under test. The `AddChildContent` method takes an optional `ComponentParameterCollectionBuilder` as input, which can be used to pass parameters to the `TChildComponent` component, which in this case is the `` component. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-mix-of-razor-and-html-to-a-childcontent-parameter)Passing a mix of Razor and HTML to a ChildContent parameter Some times you need to pass multiple different types of content to a ChildContent parameter, e.g. both some markup and a component. This can be done in the following way: ``` public class ChildContentParams4Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .AddChildContent("Below you will find a most interesting alert!") .AddChildContent(childParams => childParams .Add(p => p.Heading, "Alert heading") .Add(p => p.Type, AlertType.Warning) .AddChildContent("Hello World") ) ); } } ``` Passing a mix of markup and components to a `ChildContent` parameter is done by simply calling the `ComponentParameterCollectionBuilder`‘s `AddChildContent()` methods as seen here. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#renderfragment-parameters)RenderFragment parameters A `RenderFragment` parameter is very similar to the special `ChildContent` parameter described in the previous section, since a `ChildContent` parameter *is* of type `RenderFragment`. The only difference is the name, which must be anything other than `ChildContent`. In Blazor, a `RenderFragment` parameter can be regular HTML markup, it can be Razor markup, e.g. other component declarations, or it can be a mix of the two. If it is another component, then that component can also receive child content, and so forth. The following subsections have different examples of content being passed to the following component’s `RenderFragment` parameter: ``` public class RenderFragmentParams : ComponentBase { [Parameter] public RenderFragment Content { get; set; } } ``` #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-html-to-a-renderfragment-parameter)Passing HTML to a RenderFragment parameter ``` public class RenderFragmentParams1Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Content, "Hello World") ); } } ``` The example uses the `ComponentParameterCollectionBuilder`‘s `Add` method to pass an HTML markup string as the input to the `RenderFragment` parameter. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-component-without-parameters-to-a-renderfragment-parameter)Passing a component without parameters to a RenderFragment parameter To pass a component such as the classic `` component, which does not take any parameters, to a `RenderFragment` parameter, do the following: ``` public class RenderFragmentParams2Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Content) ); } } ``` The example uses the `ComponentParameterCollectionBuilder`‘s `Add` method, where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-component-with-parameters-to-a-renderfragment-parameter)Passing a component with parameters to a RenderFragment parameter To pass a component with parameters to a `RenderFragment` parameter, e.g. the `` component with the following parameters, do the following: ``` [Parameter] public string Heading { get; set; } [Parameter] public AlertType Type { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } ``` ``` public class RenderFragmentParams3Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Content, alertParameters => alertParameters .Add(p => p.Heading, "Alert heading") .Add(p => p.Type, AlertType.Warning) .AddChildContent("Hello World") ) ); } } ``` The example uses the `ComponentParameterCollectionBuilder`‘s `Add` method, where `TChildComponent` is the (child) component that should be passed to the `RenderFragment` parameter. The `Add` method takes an optional `ComponentParameterCollectionBuilder` as input, which can be used to pass parameters to the `TChildComponent` component, which in this case is the `` component. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-mix-of-razor-and-html-to-a-renderfragment-parameter)Passing a mix of Razor and HTML to a RenderFragment parameter Some times you need to pass multiple different types of content to a `RenderFragment` parameter, e.g. both markup and and a component. This can be done in the following way: ``` public class RenderFragmentParams4Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Content, "Below you will find a most interesting alert!") .Add(p => p.Content, childParams => childParams .Add(p => p.Heading, "Alert heading") .Add(p => p.Type, AlertType.Warning) .AddChildContent("Hello World") ) ); } } ``` Passing a mix of markup and components to a `RenderFragment` parameter is simply done by calling the `ComponentParameterCollectionBuilder`‘s `Add()` methods or using the `ChildContent()` factory methods in `ComponentParameterFactory`, as seen here. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#templates-parameters)Templates parameters Template parameters are closely related to the `RenderFragment` parameters described in the previous section. The difference is that a template parameter is of type `RenderFragment`. As with a regular `RenderFragment`, a `RenderFragment` template parameter can consist of regular HTML markup, it can be Razor markup, e.g. other component declarations, or it can be a mix of the two. If it is another component, then that component can also receive child content, and so forth. The following examples renders a template component which has a `RenderFragment` template parameter: ``` @typeparam TItem @foreach (var item in Items) { @Template(item) } @code { [Parameter] public IEnumerable Items { get; set; } [Parameter] public RenderFragment Template { get; set; } } ``` #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-html-based-templates)Passing HTML-based templates To pass a template into a `RenderFragment` parameter that just consists of regular HTML markup, do the following: ``` public class TemplateParams1Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" }) .Add(p => p.Template, item => $"{item}") ); } } ``` The examples pass a HTML markup template into the component under test. This is done with the help of a `Func` delegate which takes whatever the template value is as input, and returns a (markup) string. The delegate is automatically turned into a `RenderFragment` type and passed to the template parameter. The example uses the `ComponentParameterCollectionBuilder`‘s `Add` method to first add the data to the `Items` parameter and then to a `Func` delegate. The delegate creates a simple markup string in the example. #### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-a-component-based-template)Passing a component-based template To pass a template into a `RenderFragment` parameter, which is based on a component that receives the template value as input (in this case, the `` component listed below), do the following: ``` @Value @code { [Parameter] public string Value { get; set; } } ``` ``` public class TemplateParams2Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.Items, new[] { "Foo", "Bar", "Baz" }) .Add(p => p.Template, value => itemParams => itemParams .Add(p => p.Value, value) ) ); } } ``` The example creates a template with the `` component listed above. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#unmatched-parameters)Unmatched parameters An unmatched parameter is a parameter that is passed to a component under test, and which does not have an explicit `[Parameter]` parameter but instead is captured by a `[Parameter(CaptureUnmatchedValues = true)]` parameter. In the follow examples, we will pass an unmatched parameter to the following component: ``` public class UnmatchedParams : ComponentBase { [Parameter(CaptureUnmatchedValues = true)] public Dictionary InputAttributes { get; set; } } ``` ``` public class UnmatchedParamsTest { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .AddUnmatched("some-unknown-param", "a value") ); } } ``` The examples passes in the parameter `some-unknown-param` with the value `a value` to the component under test. ## [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#cascading-parameters-and-cascading-values)Cascading Parameters and Cascading Values Cascading parameters are properties with the `[CascadingParameter]` attribute. There are two variants: **named** and **unnamed** cascading parameters. In Blazor, the `` component is used to provide values to cascading parameters, which we also do in tests written in `.razor` files. However, for tests written in `.cs` files we need to do it a little differently. The following examples will pass cascading values to the `` component listed below: ``` @code { [CascadingParameter] public bool IsDarkTheme { get; set; } [CascadingParameter(Name = "LoggedInUser")] public string UserName { get; set; } [CascadingParameter(Name = "LoggedInEmail")] public string Email { get; set; } } ``` ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-unnamed-cascading-values)Passing unnamed cascading values To pass the unnamed `IsDarkTheme` cascading parameter to the `` component, do the following: ``` public class CascadingParams1Test { [Fact] public void Test() { using var ctx = new TestContext(); var isDarkTheme = true; var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.IsDarkTheme, isDarkTheme) ); } } ``` The example pass the variable `isDarkTheme` to the cascading parameter `IsDarkTheme` using the `Add` method on the `ComponentParameterCollectionBuilder` with the parameter selector to explicitly select the desired cascading parameter and pass the unnamed parameter value that way. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-named-cascading-values)Passing named cascading values To pass a named cascading parameter to the `` component, do the following: ``` public class CascadingParams2Test { [Fact] public void Test() { using var ctx = new TestContext(); var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.UserName, "Name of User") ); } } ``` The example pass in the value `Name of User` to the cascading parameter with the name `LoggedInUser`. Note that the name of the parameter is not the same as the property of the parameter, e.g. `LoggedInUser` vs. `UserName`. The example uses the `Add` method on the `ComponentParameterCollectionBuilder` with the parameter selector to select the cascading parameter property and pass the parameter value that way. ### [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#passing-multiple-named-and-unnamed-cascading-values)Passing multiple, named and unnamed, cascading values To pass all cascading parameters to the `` component, do the following: ``` public class CascadingParams3Test { [Fact] public void Test() { using var ctx = new TestContext(); var isDarkTheme = true; var cut = ctx.RenderComponent(parameters => parameters .Add(p => p.IsDarkTheme, isDarkTheme) .Add(p => p.UserName, "Name of User") .Add(p => p.Email, "user@example.com") ); } } ``` The example passes both the unnamed `IsDarkTheme` cascading parameter and the two named cascading parameters (`LoggedInUser`, `LoggedInEmail`). It does this using the `Add` method on the [`ComponentParameterCollectionBuilder`](https://bunit.dev/api/Bunit.ComponentParameterCollectionBuilder-1.html) with the parameter selector to select both the named and unnamed cascading parameters and pass values to them that way. ## [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#rendering-a-component-under-test-inside-other-components)Rendering a component under test inside other components It is possible to nest a component under tests inside other components, if that is required to test it. For example, to nest the `` component inside the `` component do the following: ``` public class NestedComponentTest { [Fact] public void Test() { using var ctx = new TestContext(); var wrapper = ctx.RenderComponent(parameters => parameters .AddChildContent() ); var cut = wrapper.FindComponent(); } } ``` The example renders the `` component inside the `` component. What is special in both cases is the use of the `FindComponent()` that returns a `IRenderedComponent`. This is needed because the `RenderComponent` method call returns an `IRenderedComponent` instance, that provides access to the instance of the `` component, but not the ``-component instance. ## [](https://bunit.dev/docs/providing-input/passing-parameters-to-components.html?tabs=csharp#configure-two-way-with-component-parameters-bind-directive)Configure two-way with component parameters (`@bind` directive) To set up [two-way binding to a pair of component parameters](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/data-binding#binding-with-component-parameters) on a component under test, e.g. the `Value` and `ValueChanged` parameter pair on the component below, do the following: ``` @code { [Parameter] public string Value { get; set; } = string.Empty; [Parameter] public EventCallback ValueChanged { get; set; } } ``` ``` public class TwoWayBindingTest { [Fact] public void Test() { using var ctx = new TestContext(); var currentValue = string.Empty; ctx.RenderComponent(parameters => parameters.Bind( p => p.Value, currentValue, newValue => currentValue = newValue)); } } ``` The example uses the `Bind` method to setup two-way binding between the `Value` parameter and `ValueChanged` parameter, and the local variable in the test method (`currentValue`). The `Bind` method is a shorthand for calling the the `Add` method for the `Value` parameter and `ValueChanged` parameter individually. **Categories:** Blazor, C#, Testing **Tags:** blazor --- ### [How to use Bing with ChatGPT](https://puresourcecode.com/news/how-to-use-bing-with-chatgpt/) **Published:** February 23, 2023 **Author:** Enrico **Excerpt:** Here I like to explore how to use Bing, the Microsoft search engine, with ChatGPT and the new Bing app and the integration in Edge and Skype **Content:** In the last few weeks, we heard a lot about artificial intelligence and its possible integration: here I like to explore how to use [Bing](https://www.bing.com/), the Microsoft search engine, with [ChatGPT](https://chat.openai.com/chat). Bing is getting turbocharged with an injection of OpenAI’s ChatGPT technology, transforming Microsoft’s search engine into something capable of carrying on a conversation. The news was announced at a Microsoft ChatGPT event in February 2023 where company execs confirmed that OpenAI’s next-level chatbot tech is going to be integrated into both Bing and Microsoft’s web browser Edge. > hello from redmond! excited for the event tomorrow [pic.twitter.com/b7TUr0ti42](https://t.co/b7TUr0ti42) > > — Sam Altman (@sama) [February 6, 2023](https://twitter.com/sama/status/1622689308829429760?ref_src=twsrc%5Etfw) This comes after Microsoft invested billions in OpenAI to try and challenge the search dominance of Google, which plans to launch its own Google Bard AI chatbot. There’s also a paid version of ChatGPT called ChatGPT Plus, so the AI chatbot race is really heating up. > Bing and Edge + AI: a new way to search starts today > > — Satya Nadella (@satyanadella) [February 7, 2023](https://twitter.com/satyanadella/status/1623031898544152578?ref_src=twsrc%5Etfw) ## Microsoft announcement On February 7, Microsoft announced a deeper partnership with OpenAI that sees it integrating a more powerful version of the company’s ChatGPT generative AI to improve Bing and Edge with new search and AI-powered features. Microsoft CEO Satya Nadella and OpenAI CEO Sam Altman both made an appearance at the event (which explains why Altman tweeted a photo of himself and Nadella(opens in new tab) yesterday) to reassure the audience that the “AI” tech powering Bing and Edge’s new features is safe and trustworthy, a key concern as generative AI continues to raise hackles in fields from cybersecurity to digital art creation. ![Microsoft website for ChatGPT - How to use Bing with ChatGPT](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-16.png?resize=640%2C360&ssl=1)Microsoft website for ChatGPT ## What is ChatGTP? So, the first question is: what is ChatGTP? I submitted this question to ChatGTP itself. Certainly! I am [ChatGPT](https://chat.openai.com/chat), an artificial intelligence language model developed by OpenAI. I have been trained on a large dataset of human language to understand and generate text that mimics human conversation. Essentially, I am a computer program that can communicate with people in natural language, such as English, to answer questions, provide information, or engage in conversation on a wide range of topics. As a language model, I work by processing the text input I receive and generating a response based on my understanding of the language and the context of the conversation. I can answer factual questions, provide explanations, offer advice, or just engage in casual conversation. One of the benefits of using a language model like me is that I can access a vast amount of information and knowledge on almost any topic, as long as it’s within my training dataset. However, it’s important to note that I am not perfect and may occasionally provide incorrect or incomplete information. Overall, my goal is to help users in their information-seeking or conversational needs, and I am constantly learning and improving with each interaction. ![My chat with ChatGPT - How to use Bing with ChatGPT](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-15.png?resize=640%2C379&ssl=1)My chat with ChatGPT ### Can you tell me more? As a language model, I’m built on a neural network architecture that processes text inputs and generates text outputs. My training dataset consists of a vast amount of text data, including books, articles, and web pages, which I use to learn patterns and structures of language. When you ask me a question or provide me with a statement, I analyze the text to understand its meaning and context. I can recognize things like keywords, grammatical structures, and semantic relationships between words to generate a response. I also use a technique called natural language processing (NLP) to help me understand the nuances of language, such as sarcasm, humor, and tone. One of the key benefits of using a language model like me is that I can process large amounts of text data quickly and accurately. This makes me useful for tasks such as information retrieval, language translation, and sentiment analysis. However, it’s important to note that while I am designed to mimic human conversation, I am not sentient or conscious. I am a machine learning model that has been trained to generate text based on patterns in data. While I can understand and respond to many different types of text inputs, I do not have feelings, beliefs, or personal opinions. I hope this provides you with a better understanding of what I am and what I can do. If you have any more questions, feel free to ask! ## ChatGTP with Bing If you’ve already used the new Bing on a desktop computer, then the experience will seem mostly familiar to you. The biggest difference is that instead of clicking the “Chat” option under the search bar, you just click the Bing icon at the bottom of your phone’s screen. Once you do that, you’ll have access to the chatbot AI, which operates essentially like the desktop version — with one notable change. ![Example of Bing and ChatGTP](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/bing-openai-mobile.png?resize=640%2C383&ssl=1)Example of Bing and ChatGTP The Bing app has been given support for voice commands, which is a major upgrade in terms of convenience. Just hit the microphone icon and you can start actually chatting with the chatbot, not just typing its requests. While you add the wrinkle of Microsoft potentially storing your voice data, if you’re comfortable with using voice commands on your smartphone in general, this shouldn’t be any different. ## Microsoft chatbots: Edge and Skype also get ChatGPT features As mentioned, the Bing app isn’t all that’s getting a new Bing-style upgrade. The Microsoft Edge app on mobile now also allows you to use the new Bing’s new chatbot AI. I was able to access this as well, though I found it operated the same as using the chatbot feature on the desktop browser version of the new Bing. The one app I did not get a chance to use was Skype and its new ChatGPT integration, which is much different from the Bing and Edge integrations. In the Skype app, you can now add Bing to your group conversations. You just take an existing group conversation and add the new Bing to it like you would any other contact. Bing then acts as a member of the conversation, providing answers to queries if directly mentioned. Simply just add “@Bing” to your message and Bing will respond. Microsoft says that this Skype integration is fluent in more than 100 languages and capable of translation. It can also provide answers as bullet points, traditional text or a simplified response. This seems very similar to thee Chat tones Microsoft recently announced for the Bing chatbot AI. **Categories:** Microsoft, News **Tags:** ai, artificial-intelligence, microsoft, microsoft-bing, openai --- ### [Methods not Allowed in ASP.NET Core](https://puresourcecode.com/dotnet/net-core/methods-not-allowed-in-asp-net-core/) **Published:** February 13, 2023 **Author:** Enrico **Excerpt:** If you create APIs, you can face that same Methods not Allowed in with ASP.NET Core, NET6 or NET7. **Content:** If you create [APIs](https://puresourcecode.com/tag/webapi/), you can face that same `Methods not Allowed` in with [ASP.NET Core](https://puresourcecode.com/tag/aspnet-core/), [NET6](https://puresourcecode.com/tag/net6/) or [NET7](https://puresourcecode.com/category/dotnet/net7/). The `WebDAVModule` set `PUT` and `DELETE` request methods disabled by default and due to that PUT and DELETE throw 405 errors. Such weird discovery led me to dig through the web looking for a suitable explanation, until I eventually found the cause: it seems like the culprit is the `WebDAVModule`, which seems to set `PUT` and `DELETE` request methods disabled by default. In order to get them to work, we either need to change these defaults or disable it for the whole web application, which was what we did. Here’s what we put in the `web.config` file to remove it for good: ``` ``` Also, with the latest version of .NET CORE (2.0 and above), there might be a case of no `web.config` file available at all, if that is your case then add a `web.config` file on your own. Despite the rather easy workaround, such an issue is definitely a though one, as it will easily affect most ASP.NET Core Web API and Web Applications when they get deployed on a live environment: that’s because the WebDAV module, although not supported by IIS Express, happens to be enabled in most production servers. If you are facing the same issue with multiple APIs hosted on the same server, then either you can add the above entries under `web.config` file of all the affected API’s or you can remove the below entry of `WebDAVModule` from `ApplicationHost.config` file under the module section: ``` ``` `ApplicationHost.config` can be found at > C:\\Windows\\System32\\inetsrv\\config **Categories:** .NET Core, .NET5, .NET6, .NET7, ASP.NET --- ### [Stripe Connect Express with Blazor](https://puresourcecode.com/dotnet/net-core/stripe-connect-express-with-blazor/) **Published:** February 13, 2023 **Author:** Enrico **Content:** In my previous post titled [Create Stripe Webhooks Receiver](https://puresourcecode.com/dotnet/net-core/create-stripe-webhooks-receiver/) I created a webhook and now I want to explore how to use Stripe Connect Express with [Blazor](https://puresourcecode.com/category/dotnet/blazor/). ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/stripe-payment-wallpaper.png?resize=240%2C172&ssl=1)## What is Stripe Connect? [Stripe Connect](https://stripe.com/gb/connect) allows you to develop marketplace and platform applications that can accept money and pay out to connected Stripe accounts. For example, a platform like Lyft has the ability to receive payments from a customer, retain a percentage as a platform fee, and then pay out the difference to the customer’s driver. Those drivers would all be considered Stripe Connect accounts for the Lyft platform. So, there are three ways to integrate with Stripe Connect. A Standard account is the easiest to integrate, but it has limitations with respect to branding and the type of charges it can handle. A Custom account offers the most control, but it requires significantly more integrations work. An Express account is a happy medium between the two alternatives, and it is the focus of this tutorial. ## Configure Stripe First, like the [Stripe webhooks](https://wellsb.com/csharp/aspnet/stripe-net-create-stripe-webhooks-receiver/https://puresourcecode.com/dotnet/net-core/create-stripe-webhooks-receiver/) tutorial, this tutorial will rely on the [Stripe.net NuGet package](https://github.com/stripe/stripe-dotnet). Install it and add it to your server-side Blazor project. Then, we have to read the configuration from the `appsettings.json`: for that, I added the following file ``` { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "Stripe": { "ApiKey": "sk_test_xxxx", "ClientId": "" } } ``` Now, I’m going to read the configuration from the `Program.cs` and configure Stripe ``` var configuration = builder.Configuration; var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); configuration .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env}.json", true, true); var app = builder.Build(); string? stripeKey = builder.Configuration["Stripe:ApiKey"]; string? stripeClientId = builder.Configuration["Stripe:ClientId"]; StripeConfiguration.ApiKey = stripeKey; StripeConfiguration.ClientId = stripeClientId; ``` In order to obtain the `ClientId` for Stripe Connect, you have to fill few forms like in the following screenshot. ![Get started with Connect form - Stripe Connect Express with Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-15.png?resize=640%2C378&ssl=1)Get started with Connect form Remember that if you want to secure your data, Visual Studio offers you way as I explained in [this post](https://puresourcecode.com/dotnet/net-core/keep-secrets-out-of-source-code/). To set up your Stripe Connect, you have to review the **Connect settings**. ![Stripe Connect settings](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-17.png?resize=640%2C449&ssl=1)Stripe Connect settings Here, you have the opportunity to customize the look and feel of the Stripe page for the clients, define from what countries you accept payments, how you want to be paid and other custom functionalities. In the following screenshot, you see the list of countries to set up. ![Set up your Express account - Country](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-16.png?resize=640%2C355&ssl=1)Set up your Express account – Country Then, you have to set the branding such as Business name, colours, icons and so on. ![Stripe Connect Branding](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-18.png?resize=640%2C497&ssl=1)Stripe Connect Branding In the **Integration** section, you find your **Live mode client ID** to use in your settings, you can enable an OAuth flow so users can login with Stripe, and also the redirect URLs after the login. ![Stripe Connect Integration](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-19.png?resize=640%2C355&ssl=1)Stripe Connect Integration **Categories:** .NET, .NET Core, .NET General, .NET5, .NET6, .NET7, Blazor, C# **Tags:** aspnet-core, blazor-server, blazor-webassembly, stripe --- ### [Getting started with Java](https://puresourcecode.com/programming-languages/getting-started-with-java/) **Published:** February 1, 2023 **Author:** Enrico **Excerpt:** In this new post "Getting started with Java", I share with you my consideration about this programming language in comparison to C# **Content:** In this new post “Getting started with Java”, I share with you my consideration about this programming language in comparison to [C#](https://puresourcecode.com/category/dotnet/csharp/). As a [.NET](https://puresourcecode.com/category/dotnet/) developer, I love C# but I want also to be open-minded towards Java or other languages at least to understand if there are more skills I can add to myself. ## Stackoverflow technologies 2022 As you know, Stackoverflow publishes every every an extensive survey about people and technologies. The survey for 2022 is available [here](https://survey.stackoverflow.co/2022/). In this survey, we see the distribution of the technologies among all the respondents (professional developers and students). So, the most popular language is JavaScript, HTML and CSS, in 6th position Java and then at the 8th position C#. Then, PHP and other languages. Out of curiosity, [Swift](https://puresourcecode.com/tag/swift/) is at the 19 place with 4.91% and [R](https://puresourcecode.com/category/programming-languages/r/) at the 20th place with 4.66%. If iOS and macOS are very popular, how do developers create their apps for iOS? There is no answer in this survey. ![Stackoverflow - Programming, scripting, and markup languages: all respondents - Getting started with Java](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image.png?resize=640%2C303&ssl=1)Stackoverflow – Programming, scripting, and markup languages: all respondents Now, if we have a look only at the professional developers, the scenario is slightly different. Java is at the 6th place and C# follows. The gap is about 4%, not very big. ![Stackoverflow - Programming, scripting, and markup languages: all respondents - Getting started with Java](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-1.png?resize=640%2C303&ssl=1)Stackoverflow – Programming, scripting, and markup languages: Professional Developers ## What makes Java interesting? Java was developed by a group of developers at Sun Microsystems in 1995 and is now owned by Oracle. The goal of the developers who designed Java was to create a language that could run on appliances. They were already thinking ahead to the time we live in now, where houses are full of smart devices and smart appliances. This was one of Java’s selling points — that you could write your code once in the language and run that code anywhere. Not just on every operating system but every type of hardware. However, it would be quite a few decades before smart devices became a way of life. Despite this, Java still became popular and for a completely different reason. Java was released about the same time the Internet was born. Java had a feature called applets that could run inside a web browser. A lot of web applications were built in Java when most websites still consisted of static pages. Java gave web developers the ability to build dynamic websites that reacted to user input, and its popularity took off from there. Java was also based on the C and C++ programming languages, which were very popular. C++ was usually the programming language taught in computer science courses in those days, so many programmers were familiar with it, even today. They could apply that knowledge of C++ to programming in Java. ## What is Java used for? Java can be used in many different applications, but here are the most popular ways the language is used: ### Android mobile apps Java is the official language for Android mobile app development. In fact, the Android operating system itself is written in Java. Even though Kotlin has recently become an alternative to using Java for Android development, Kotlin still uses the Java Virtual Machine and can interact with Java code. Today, Android has [85% of the global market share](https://www.idc.com/promo/smartphone-market-share) for mobile devices. Therefore, developing Android apps is probably the most popular use of Java just because of the prevalence of Android phones. The Java programming language can be considered as the official language for mobile application development. Most of the android applications build using Java. The most popular android app development IDE **Android Studio** also uses Java for developing android applications. So, if you are already familiar with Java, it will become much easier to develop android applications. The most popular android applications **Spotify** and **Twitter** are developed using Java. ### Desktop applications Java has been used to create desktop applications since its inception. AWT, Swing, and JavaFX are Java libraries that give desktop application developers pre-built components like buttons, menus, and form fields that they can use to build full-featured desktop applications. ### Web applications Java first became popular as a web development language because it provides applets that can run in a web browser. Applets are a thing of the past, but Java is still very popular for creating back-end web applications, which run on a web server. Now web developers use Java technologies like Struts, Servlets, or JSP instead of Applets to create all types of full-featured web applications. Also, it provides vast support for web development through Servlet, JSP, and Struts. It is the reason that Java is also known as a server-side programming language. Using these technologies, we can develop a variety of applications. The most popular frameworks Spring, Hibernate, Spring Boot, used for developing web-based applications. **LinkedIn, AliExpress, web.archive.org, IRCTC,** etc. are the popular websites that are written using Java programming language. ### Game development Java is a free, open-source language. Many game developers use it because they can get started without paying any licensing fees and because of the powerful Java 3D game engine, JMonkeyEngine. Some video games written in Java include Tetris, The Sims 3, Space Invaders, Street Fighter II, and Contra. One of the best-known Java games, Minecraft, was created by a single developer. ### Big data processing Java and big data go hand in hand. Many of the top applications used for big data are written in Java. Hadoop is a Java framework that helps data scientists process large datasets. Spark is a tool that data scientists use for stream processing, machine learning analytics, and other big data processes. Storm handles real-time data streams. All of these frameworks are written in the Java. ### IoT applications Java was originally designed to run on all types of hardware, making it one of the main programming languages used for the Internet of Things, or IoT. IoT refers to a network of physical devices that connect and exchange data over the Internet. The devices include smartwatches, wearables, smart TVs, smart lighting, and more. ### Distributed applications Many distributed applications run in a cloud environment and are designed to scale when the load changes. But distributed applications are not necessarily easy to deploy and manage. Java provides the Java Intelligent Networking Infrastructure or JINI to make distributing applications simpler. JINI is an infrastructure to provide, register, and manage distributed Java applications. ### Cloud-based applications Java is also heavily used in cloud-based applications. Because of its low cost and wide use, many companies use it to develop SaaS, IaaS, and PaaS services in the cloud. ### Enterprise Development Java is used heavily in enterprise development to build intranets and internal software for all types of businesses, big and small. Java Enterprise Edition (Java EE) is specifically designed for enterprise development. It comes with network applications, web services, and a scripting environment that make setting up an intranet with Java simpler. ### Blu-ray Finally, blu-ray is a technology heavily based on Java. BD-J, or Blu-ray Disc Java, is a specification supporting Java ME (specifically the Personal Basis Profile of the Connected Device Configuration or CDC) Xlets for advanced content on Blu-ray Disc and the Packaged Media profile of Globally Executable MHP (GEM). BD-J allows bonus content on Blu-ray Disc titles to be far more sophisticated than bonus content provided by standard DVD, including network access, picture-in-picture and access to expanded local storage. Collectively, these features (other than internet access) are referred to as “Bonus View”, and the addition of internet access is called “BD Live”. BD-J was developed by the Blu-ray Disc Association. All Blu-ray Disc players supporting video content are required by the specification to support BD-J. Starting on October 31, 2007, all new players are required to have hardware support for the “Bonus View” features, but the players may require future firmware updates to enable the features. “BD Live” support is always optional for a BD player. Sony’s PlayStation 3 has been the de facto leader in compliance and support of BD-J. The PlayStation 3 added Blu-ray Profile 1.1 support with a firmware upgrade and was used to showcase BD-Live at CES 2008 in January. ## Top Companies that Use Java There is a majority of companies such as, **Uber, Pinterest, Google, Instagram, Spotify, Netflix, Airbnb,** etc. that use Java in their tech stack. We have listed some companies or organizations and their projects. It will help you to decide which programming language you have to choose for the next project. ### NASA Word Wind NASA Word Wind is the project of an independent agency of the U.S. federal government NASA. It is a fully 3D virtual globe that provides geographic information. It uses imagery and aerial photography received from the NASA satellite and builds 3D models of the planets. Also, it is an open-source proprietary software written in Java and supports all operating systems. In this project, the OpenGL API is used to provide 2D and 3D graphics that interact with the graphics processing unit. It also shows the data in real-time by using the GPS plugin such as displaying clouds, hurricanes, earthquakes, etc. Using this application, we can search for locations by geographical names, set visible layers and viewing angles, and much more. ### Netflix Netflix is one of the most popular and largest US entertainment company that provides movies and TV shows on streaming multimedia. Most of the applications of Netflix is developed using Java. With a slight mixture of C++, android and android TV applications are almost build in Java. ### Spotify Spotify is an online audio streaming service that uses Java to implement the functionality of its web application. For example, log and stabilize, and data transfer. The android application of Spotify uses Java. ### Minecraft Minecraft is a famous computer game that is written in Java. The Minecraft Java edition comes with Java 1.8 and Minecraft used it by default. ## Get ready for Java ### Install a Java Development Kit (JDK) A Java Development Kit (JDK) is a bunch of software that makes all Java programs work. To install a Java Development Kit, you have few choises: - Download the official JDK from [Oracle](https://www.oracle.com/java/technologies/downloads/#jdk19-windows) - Visit the website [adoptium.net](https://adoptium.net/) and follow that website’s instruction If Oracle is the official one and the “creator” of Java (the real creator was Sun Microsystem acquired by Oracle), why do I have to use adoptium? The problem with Oracle’s official version is that it comes with a long, somewhat confusing list of legal requirements. Plus, the cost of [Oracle Java licence is raising](https://www.infoworld.com/article/3686611/oracle-per-employee-java-pricing-causes-concern.html). ### Install an integrated development environment Now, getting started with Java requires an integrated development environment (IDE). IDE is a program to help you compose and test new software. It is like [Visual Studio](https://puresourcecode.com/category/tools/visual-studio-tools/). Here’s a list of the IDE’s that are most popular among professional developers: - [Eclipse](https://www.eclipse.org/downloads/) - [IntelliJ IDEA](https://www.jetbrains.com/idea/) - [NetBeans](https://netbeans.apache.org/) - [Visual Studio Code](https://code.visualstudio.com/) ### Test your environment So, my choice is Eclipse only because it is the historical IDE for Java. When you launch Eclipse, it asks already a lot of things about the environment, where to place files and workspace and so on. I select the default for every choice. Now, finally Eclipse starts. What I notice immediately is that the help is not helping you: the instruction doesn’t have the correct instruction. This makes me crazy! Welcome to Java! ![Eclipse at the first start](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-2.png?resize=640%2C409&ssl=1)Eclipse at the first start ### Create the first project First, we have to create a new **Java Project**. Click on the menu **File**, then **New** and then **Java Project**. ![Create a new Java project](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-3.png?resize=640%2C691&ssl=1)Create a new Java project Next, we see this easy window to configure the new project. I only add the **Project name** at the top and I accept all the other default values. Then, click **Next** that creates the project and show some info about it and then **Finish**. ![New Java project window](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-4.png?resize=640%2C691&ssl=1)New Java project window So, it is time to create a Java Class where we add the code to print as an output a simple string. Again, from the menu **File > New** select **Class**. You see a window like the following. ![Create a new class](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-7.png?resize=640%2C679&ssl=1)Create a new class Remember to check the option for `public static void main(String[] args)`. As the name add `HelloWorld` and click **Finish**. In the new class, add the line to print a value in the console. ``` public class HelloWorld { public static void main(String[] args) { System.out.print(12345); } } ``` Now, press the **Run** button. So, Eclipse shows me a **Save and Launch** window asking me what resource to save. I save everything. ![Save and Launch](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-6.png?resize=405%2C484&ssl=1)Save and Launch There is already an error. No comment. I understand that there is a conflict between the `HelloWorld.java` and the `module-info.java`. So, we have to delete the `module-info.java` and then the program starts. ## What is Java Bytecode? Java bytecode is the instruction set for the Java Virtual Machine. It acts similar to an assembler which is an alias representation of a C++ code. As soon as a java program is compiled, java bytecode is generated. In more apt terms, java bytecode is the machine code in the form of a .class file. With the help of java bytecode we achieve platform independence in java. ### How does it works? When we write a program in Java, firstly, the compiler compiles that program and a bytecode is generated for that piece of code. When we wish to run this .class file on any other platform, we can do so. After the first compilation, the bytecode generated is now run by the Java Virtual Machine and not the processor in consideration. This essentially means that we only need to have basic java installation on any platforms that we want to run our code on. Resources required to run the bytecode are made available by theJava Virtual Machine, which calls the processor to allocate the required resources. JVM’s are stack-based so they stack implementation to read the codes. ![Java Bytecode Generic approach](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/java-bytecode.png?w=640&ssl=1)Java Bytecode Generic approach ### Advantage of Java Bytecode Platform independence is one of the soul reasons for which James Gosling started the formation of java and it is this implementation of bytecode which helps us to achieve this. Hence bytecode is a very important component of any java program. The set of instructions for the JVM may differ from system to system but all can interpret the bytecode. A point to keep in mind is that bytecodes are non-runnable codes and rely on the availability of an interpreter to execute and thus the JVM comes into play. Bytecode is essentially the machine level language which runs on the Java Virtual Machine. Whenever a class is loaded, it gets a stream of bytecode per method of the class. Whenever that method is called during the execution of a program, the bytecode for that method gets invoked. Javac not only compiles the program but also generates the bytecode for the program. Thus, we have realized that the bytecode implementation makes Java a **platform-independent** language. This helps to add portability to Java which is lacking in languages like C or C++. Portability ensures that Java can be implemented on a wide array of platforms like desktops, mobile devices, severs and many more. Supporting this, Sun Microsystems captioned JAVA as *“write once, read anywhere” or “WORA”* in resonance to the bytecode interpretation. ### Example Consider the following Java code: ``` outer: for (int i = 2; i < 1000; i++) { for (int j = 2; j < i; j++) { if (i % j == 0) continue outer; } System.out.println (i); } ``` A Java compiler might translate the Java code above into bytecode as follows, assuming the above was put in a method: ``` 0: iconst_2 1: istore_1 2: iload_1 3: sipush 1000 6: if_icmpge 44 9: iconst_2 10: istore_2 11: iload_2 12: iload_1 13: if_icmpge 31 16: iload_1 17: iload_2 18: irem 19: ifne 25 22: goto 38 25: iinc 2, 1 28: goto 11 31: getstatic #84; // Field java/lang/System.out:Ljava/io/PrintStream; 34: iload_1 35: invokevirtual #85; // Method java/io/PrintStream.println:(I)V 38: iinc 1, 1 41: goto 2 44: return ``` In any case, you don’t have to understand the code in the bytecode or change it. So, we don’t care so much about it but we know it is exist. ## What is the JVM? The JVM in simple words is an engine that reads compiled code in a format that is specified from a Java Virtual Machine Specification and executes it on the current machine. The advantages of this approach is mainly cross-platform compatibility, as the compiled code, which is called bytecode, is supposed to be platform agnostic. That means that the code compiled in a Linux machine and the code compiled in a Windows machine should work in the JVM either ways. We can copy the compiled `.class` files from linux to windows and run them there without issues and vice versa. In other words, when you install Java on your Windows PC, the `java` tool will use a platform specific runtime and a JIT compiler to run your code on Windows. The `javac` on the other-hand will compile your `.java` files to the generic bytecode format. The Bytecode itself is a format that follows a specification from the Java Virtual Machine Specification. It has various features enabled based on the current version. Those features are dictated based on JSR’s or Java Specification Requests and based on the current implementation. ## Add some documentation Now, look at this code ``` /* * Example of a Java code */ /** * The HelloWorld class displays text * on the computer screen * * @author Enrico Rossini * @version 1.0 01/02/2023 * @see java.lang.System */ public class HelloWorld { /** * The main method is where * execution of the code begins * * @param args Generic arguments */ public static void main(String[] args) { System.out.print("This is a test"); // Replace 12345 with This is a test } } ``` A comment is a special section of text, inside the program, whose purpose is to help people understand the program. There are 3 types of comment: - **traditional comments**: at the top of the file, you see this type of comment. The comment begin with `/*` and ends with `*/`. Everything between the opening `/*` and the closing `*/`is for human eyes only. - **end-of-line comments**: the `// Replace 12345 with This is a test` is an end-of-line comment. An end-of-line comment starts with 2 slashes and goes to the end of a line of type.The compiler doesn’t translate the text inside the end-of-line comment - **Javadoc comments**: a `javadoc` comment begins with `/**`. This is a special kind of traditional comment and it is meant to be read by people. ### Javadoc Now, all IDE provides a way to generate the documentation starts with the comment in your code and they look the same. To generate the documentation, on Eclipse, click on the menu **Project** and then **Generate Javadoc**. ![Generate Javadoc](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-10.png?resize=640%2C691&ssl=1)Generate Javadoc Now, a new window opens with the options you want for the documentation. Here, I just click on **Finish**. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-11.png?resize=640%2C525&ssl=1) In the hierarchy of the project, there is not a new `doc` folder that contains all the HTML and CSS file for the documentation. If you click in the `index.html`, you see a documentation like that. ![The javadoc page generated from the code](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/02/image-9.png?resize=640%2C650&ssl=1)The javadoc page generated from the code **Categories:** Java, Programming languages **Tags:** java **Hashtags:** java --- ### [Configure RStudio in Azure with Ubuntu](https://puresourcecode.com/tools/azure-tools/configure-rstudio-in-azure-with-ubuntu/) **Published:** November 24, 2021 **Author:** Enrico **Excerpt:** I like to explain how to configure RStudio in Azure with an Ubuntu virtual machine running a script at the startup. Quick and easy. **Content:** In this new post, I like to explain how to configure RStudio in [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/) with an Ubuntu virtual machine. In the last few months, I worked a lot on R, RStudio and the integration with Azure. I have created those posts: - [Getting started with R](https://puresourcecode.com/programming-languages/getting-started-with-r/) - [Deploying dockerized R/Shiny Apps on Microsoft Azure](https://puresourcecode.com/programming-languages/r/deploying-dockerized-r-shiny-apps-on-microsoft-azure/) - [Deploy ShinyApps with Azure and Docker](https://puresourcecode.com/programming-languages/r/deploy-shinyapps-with-azure-and-docker/) Also, I worked on the integration between R/Plumber, the library for creating API with R, and Azure using the API Management Service - [How to use an Azure API Management Service](https://puresourcecode.com/tools/azure-tools/how-to-use-an-azure-api-management-service/) In Azure I have a very expensive virtual machine with [Ubuntu 20.04](https://ubuntu.com/download/desktop?version=20.04&architecture=amd64) because I need a power machine for long and complex calculation. I shutdown the machine when I finish my stuff. When I restart the machine, I have to open again the connection with the [Azure Container Repository](https://puresourcecode.com/programming-languages/r/deploying-dockerized-r-shiny-apps-on-microsoft-azure/) (ACR). Then, restart the Docker container with [RStudio](https://puresourcecode.com/tag/rstudio/). So, I don’t want to do everything manually and I started to try to run automatically all the command at the startup. I tried and I found difficult to configure services in Ubuntu, mostly because Ubuntu removed some commands. After few days, I found the way and I want to share it with you. From the beginning… ## Configure the Virtual Machine (VM) As I said, I created a virtual machine in Azure with Ubuntu 20.04. I don’t explain how to create a virtual machine in this post. If you need help, see this [post](https://puresourcecode.com/tools/azure-tools/deploy-shinyapps-with-azure-and-docker/). So, the machine is ready and I have access to it via SSH. So, the first thing I want to set up is: - Docker - RDP (Remote Desktop Protocol) to connect to the virtual machine - Connection with Azure Container Registry - Start RStudio For that, I prepare a Bash script to run. To create a script to execute, you can follow these steps. There is a simple editor and its name is **Nano**. It is easy to use and it is already installed. First, open a SSH connection with the virtual machine. Then, I’m going to create the file `first.sh` typing this command ``` nano first.sh ``` In this file I added all the command I need to set up all the above applications. This is the script: ``` sudo apt-get update sudo apt install docker.io sudo systemctl start docker sudo systemctl enable docker sudo apt-get -y install xfce4 sudo apt-get -y install xrdp sudo systemctl enable xrdp echo xfce4-session >~/.xsession sudo service xrdp restart sudo docker login youracr.azurecr.io --username yourusername --password yourpassword sudo docker run -d -p 8787:8787 -e USER=rstudio -e PASSWORD=mypassword youracr.azurecr.io/rstudio ``` So, you end up to have a screen like the following image. ![Nano editor via SSH in Ubuntu - Configure RStudio in Azure with Ubuntu](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/image-8.png?resize=640%2C351&ssl=1)Nano editor via SSH in Ubuntu Now, you have to tell Ubuntu that this file is executable. For that, you have to run this command: ``` chmod +x first.sh ``` Now, you run the script with this command ``` ./first.sh ``` Another option is as follows to execute shell script with one of the following commands: ``` sh script-name-here.sh bash script-name-here.sh ``` Now, the virtual machine is ready. If I restart the machine, Docker doesn’t start. So, next step is to configure a service that start at the startup to execute Docker. ## Configure a service at the startup with Ubuntu 20.04 So, when I started, I didn’t know that Ubuntu removed `chkconfig` to configure a new service or that it requires file with [LBS information](https://wiki.debian.org/LSBInitScripts). I won’t tell you all the story but basically, for every Ubuntu there is some different configuration to apply. The solution with Ubuntu 20.04 is pretty straightforward: I can add the command in `/etc/rc.local` So, move to the `etc` folder and with `nano` editor open the file `rc.local` (probably using `sudo`) ``` sudo nano /etc/rc.local ``` This executes the commands as root. To execute commands as a specific user, use `sudo -i -u` (`-i` to also run the login shell). For example, to establish a persistent SSH tunnel, where `myhost` is definde in `johndoe`s `~/.ssh/config` file: ``` sudo -i -u johndoe autossh -nNT -L 1234:localhost:1234 myhost ``` Sometimes, the file `rc.local` didn’t exists. So, you have a blank file in nano. For that, you have to add at the beginning of the file the following code called **Shebang line**. ``` #!/bin/bash ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/08/image-6.png?resize=640%2C353&ssl=1)Example of rc.local I added the last line in the file because when Docker runs the second line but a container already exists, it raises an error. So, the third line runs Docker with the local image. ### How to start the container A full example of the `/etc/rc.local` is the following one ``` sudo docker login youracr.azurecr.io --username yourusername --password yourpassword sudo docker rm rstudio sudo docker run -d -p 8787:8787 -e USER=rstudio -e PASSWORD=mypassword youracr.azurecr.io/rstudio ``` So, when the machine restarts, Docker logins to the ACR, remove the `rstudio` instance, if it exists, and then runs the new instance. ### Check the permission of rc.local Last thing is about `/etc/rc.local`: be sure it is executable: ``` sudo chown root /etc/rc.local sudo chmod 755 /etc/rc.local ``` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/08/image-5.png?resize=640%2C353&ssl=1)rc.local is in green If you have to change the `rc.local` file and you applied the `755` you have to change it: ``` sudo chmod 777 /etc/rc.local ``` **Categories:** Azure, Programming languages, R, Ubuntu **Tags:** azure, linux, linux-ubuntu, rstudio, ubuntu, virtual-machine --- ### [Create Stripe webhooks receiver](https://puresourcecode.com/dotnet/net-core/create-stripe-webhooks-receiver/) **Published:** January 19, 2023 **Author:** Enrico **Excerpt:** I create a Stripe webhooks receiver for ASP.NET Core and Blazor. This is the first post of 4 where I show the full implementation. **Content:** In this post, I create a Stripe [webhooks](https://learn.microsoft.com/en-us/aspnet/webhooks/) receiver for [ASP.NET Core](https://puresourcecode.com/tag/aspnet-core/) and [Blazor](https://puresourcecode.com/tag/blazor/). This is the first post of 4 where I show the full implementation. ## What is a webhook? WebHooks is a lightweight HTTP pattern providing a simple pub/sub model for wiring together [Web APIs](https://puresourcecode.com/tag/webapi/) and SaaS services. When an event happens in a service, a notification is sent in the form of an HTTP POST request to registered subscribers. The POST request contains information about the event which makes it possible for the receiver to act accordingly. So, Stripe is one of the provider that offers a webhook for its system. ## Configure Stripe First, we have to configure a new account in the [Stripe dashboard](https://dashboard.stripe.com/) after your registration. For this example, I’m going to create a new **Account Name** with name `PSC Test` and the country is `United Kingdom`. ![Create new account on Stripe dashboard - Create Stripe webhooks receiver](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-1.png?resize=618%2C318&ssl=1)Create new account on Stripe dashboard After that, you should be in the same environment as in the following screenshot. The new account is in `Developers` status and `Test mode` is activated. ![Generate an API key in the Stripe dashboard - Create Stripe webhooks receiver](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-2.png?resize=640%2C363&ssl=1)Generate an API key in the Stripe dashboard Now, on the left side you see the **Developers** menu. Under **API Keys**, you find the keys to use in the project later. To test the webhook, you have to download the [Stripe CLI from GitHub](https://github.com/stripe/stripe-cli/releases) for your operating system. The CLI will help us to test the application. Download the file (it is just an `exe` for Windows) and save it in the project folder. ## Configure your ASP.NET Core project Now, the next step is to configure your ASP.NET Core project in order to receive the call to the webhook from Stripe. In my case, I created a solution for a Blazor application hosted in ASP.NET Core website. Therefore, I have to add the `Stripe.net` as a [NuGet package](https://www.nuget.org/packages/Stripe.net/) to the solution. ![Add Stripe.net as NuGet package to your projects - Create Stripe webhooks receiver](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-3.png?resize=640%2C284&ssl=1)Add Stripe.net as NuGet package to your projects There are a number of global configuration settings for `Stripe.net`. The one you will need to set for this tutorial is your **Stripe API Key**. Remember, you should never store secrets in your project’s source code. Using ASP.NET’s [Secret Manager](https://puresourcecode.com/dotnet/net-core/keep-secrets-out-of-source-code/), add your API key. Also, I added my webhook endpoint signing key. ``` { "Stripe": { "ApiKey": "sk_test_xxxx", "WebhookSigningKey": "whsec_xxxx" } } ``` ### Add dependency for StripeOptions At this point, we have to save an read the configuration for Stripe. For this reason, I create a model with 2 properties: `ApiKey` and `WebhookSigningKey`. ``` namespace BlazorStripe.Shared.Models { public class StripeOptions { public string? ApiKey { get; set; } public string? WebhookSigningKey { get; set; } } } ``` Now, I have to read those values in the `Program.cs` and add the dependency for the project and set the `ApiKey` to Stripe. ``` builder.Services.AddTransient(_ => { return builder.Configuration.GetSection("Stripe").Get(); }); string? stripeKey = builder.Configuration["Stripe:ApiKey"]; StripeConfiguration.ApiKey = stripeKey; ``` ## Add the controller for the webhook receiver So, your webhook listener simply be a controller with a single `HttpPost` endpoint. Stripe will send POST requests to this endpoint containing details related to a Stripe event. Your controller will determine which type of event it has received and take the appropriate action based on the event. Now, create a **Controllers** folder in your project. Within that folder, create a class called **StripeWebhook**. You will make this a controller class that will be able to respond to any event received from Stripe’s webhooks. Now, inject any services you will need in the controller using C# dependency injection. Also inject the `StripeOptions` interface so you will be able to access your unique webhook signing key. ``` private readonly string _webhookSecret; public StripeWebhook(StripeOptions options) { _webhookSecret = options?.WebhookSigningKey; } ``` In the constructor above, I have extracted the `WebhookSigningKey` value from the Options interface and assigned it to a private variable `_webhookSecret`. Next, add a single `HttpPost` endpoint for your controller. This is where you will respond to events sent by Stripe. When you are designing your webhook receiver, please remember that Stripe expects your application to send an Http success response code every time an event is received. Stripe will notify your webhook receiver for any numbers of events. For example, your application might respond when a customer updates their payment information, when a customer’s payment method will soon expire, or when a charge fails. Your controller should determine which type of event it has received and then take the appropriate action. ``` [HttpPost] public async Task Index() { string json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync(); try { var stripeEvent = EventUtility.ConstructEvent(json, Request.Headers["Stripe-Signature"], _webhookSecret); switch (stripeEvent.Type) { case Events.CustomerSourceUpdated: //make sure payment info is valid break; case Events.CustomerSourceExpiring: //send reminder email to update payment method break; case Events.ChargeFailed: //do something break; } return Ok(); } catch (StripeException e) { return BadRequest(); } } ``` The controller action above starts by parsing the body of the request received and saving it as a string `json`. Then, the controller verifies that the event was sent by Stripe by comparing the value of the Stripe-Signature with our unique webhook secret. Finally, we determine the type of event and perform the appropriate action based on the event. In this case, we might have some code that will verify that a customer’s payment method is still valid and update our database records accordingly when a `customer.source.updated` event is received. Similarly, we might send a friendly reminder email in response to the `customer.source.expiring` event when a customer’s payment method is set to expire. ### Add Swagger Now, I like to add [Swagger](https://puresourcecode.com/?post_tag=swagger) to the project to see the APIs and, if it is the case, test them. So, in the server project add the NuGet package [Swashbuckle.AspNetCore.SwaggerUI](https://www.nuget.org/packages?q=Swashbuckle.AspNetCore) and Swashbuckle.AspNetCore, if not automatically added. Then, in the Program.cs add ``` // ... builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "ASP.NET Blazor with Stripe Webhooks", Version = "v1" }); }); var app = builder.Build(); ``` an then this code ``` if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Blazor with Stripe Webhooks"); }); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } ``` This configuration should be enough to see the Swagger documentation using the URL ``` https://localhost:7110/swagger ``` The result of this is the following screenshot. ![Swagger for the Stripe webhooks](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-9.png?resize=640%2C620&ssl=1)Swagger for the Stripe webhooks ### The new Program.cs ``` using BlazorStripe.Shared.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.OpenApi.Models; using Stripe; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); #region Read configuration var configuration = builder.Configuration; var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); configuration .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env}.json", true, true); StripeOptions settings = new StripeOptions(); builder.Configuration.Bind(settings); #endregion Read configuration #region Dependecy injection builder.Services.AddTransient(_ => { return builder.Configuration.GetSection("Stripe").Get(); }); #endregion string? stripeKey = builder.Configuration["Stripe:ApiKey"]; StripeConfiguration.ApiKey = stripeKey; builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "ASP.NET Blazor with Stripe Webhooks", Version = "v1" }); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ASP.NET Blazor with Stripe Webhooks"); }); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run(); ``` ### Test your controller So, it is time to test your `StripeWebhook` controller. With the `Stripe CLI` that we downloaded before, we will test all our code. Now, we have to pair the `Stripe CLI` with our account on Stripe. Open the PowerShell (or another prompt) and execute the following line ``` .\stripe.exe login ``` ![Launch Stripe CLI with PowerShell](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-6.png?resize=640%2C361&ssl=1)Launch Stripe CLI with PowerShell At this point, the CLI asks to press `Enter` to open a browser and pairs itself with the account in the Stripe Dashboard. After the `Enter`, we should see a screen like the following ![Request from Stripe CLI to be paired with your Stripe account](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-5.png?resize=640%2C438&ssl=1)Request from Stripe CLI to be paired with your Stripe account Click on **Allow access**. Then, your screen changes and the access is granted. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-7.png?resize=640%2C438&ssl=1) Then, in your PowerShell, you see the Stripe CLI is configured for your application. ![Stripe CLI is configured for your account](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-8.png?resize=640%2C361&ssl=1)Stripe CLI is configured for your account Now, you have to configure the Stripe CLI to receive the call from Stripe and forward the call to the controller `StripeWebhook` that we have just created. For that, in the PowerShell type ``` .\stripe.exe listen --forward-to https://localhost:7110/api/StripeWebhook ``` ![Stripe CLI is listenning for webhooks](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-10.png?resize=640%2C361&ssl=1)Stripe CLI is listenning for webhooks As you can see, this step gives you the webhook signing secret. Copy this key and paste in the configuration in the property `WebhookSigningKey`. Leave this PowerShell open. Now, run your server project. When the project is up and running, we are in the position to receive a trigger from Stripe. I want to try a successful payment. So, in a new PowerShell, type ``` .\stripe trigger payment_intent.succeeded ``` If you follow all steps, you receive a `Trigger succeeded`. You can add breakpoints in your code to check what your controller receives. In the Stripe CLI, you read the successful request. ![Call the Stripe's trigger from PowerShell](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-12.png?resize=640%2C361&ssl=1)Call the Stripe’s trigger from PowerShell Now, in the open PowerShell, you have all details about the webhook call. You see that the event `payment_intent.succeeded` has 3 steps: `charge.succeeded`, `payment_intent.succeded` and `payment_intent.created`. You can write in the controller for each step the appropriate code to respond to your need. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-11.png?resize=640%2C361&ssl=1) Also, all the events are available in the Stripe Dashboard. Under the option `Events`, you can see all the calls to the webhook. ![All trigger/transaction on the dashboard](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-13.png?resize=640%2C363&ssl=1)All trigger/transaction on the dashboard So, if you want to see more details about each event, just click on it. For example, I want to see all the details of the payment (first line in the above screenshot). I click on it and the result is in the following screnshot. ![Transaction detail in the Stripe dashboard](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image-14.png?resize=640%2C454&ssl=1)Transaction detail in the Stripe dashboard **Categories:** .NET Core, .NET General, .NET5, .NET6, .NET7, Blazor, C# **Tags:** aspnet-5, aspnet-core, netcore, webapi --- ### [Keep secrets out of source code](https://puresourcecode.com/dotnet/net-core/keep-secrets-out-of-source-code/) **Published:** January 19, 2023 **Author:** Enrico **Excerpt:** Keep secrets out of your source code in Visual Studio. It is never a good idea to store secrets or passwords in your source code. **Content:** In this post, I show how to keep secrets out of your source code in [Visual Studio](https://puresourcecode.com/tag/visual-studio/) from version 2019 or above. It is never a good idea to store secrets or passwords in your project’s source code. In an [ASP.NET Core](https://puresourcecode.com/tag/aspnet-core/) development environment, you can use the [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows) tool to store sensitive data. ## Introduction to Secret Manager The Secret Manager tool stores your application secrets in a `secrets.json` file located in your development machine’s `%appdata%\Microsoft\UserSecrets\\` directory. So, the secret Manager is only intended to be used in a development environment. It does not encrypt the stored keys and values. Once your app is in production, you can, of course, use a service like the [Azure Key Vault](https://puresourcecode.com/dotnet/azure/save-and-retrieve-secret-from-azure-keyvault/). ## Using Secret Manager You can access secrets stored using Secret Manager the same way you would pull data from your `appsettings.json` file. However, because the secrets are not stored in your project’s directory, you run less risk of accidentally checking your passwords in to a source control repository. ![Manage User Secrets in Visual Studio 2022 - ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2023/01/image.png?resize=571%2C906&ssl=1)Manage User Secrets in Visual Studio 2022 To enable the Secret Manager utility, open your project in Visual Studio. Then, locate your project in the Solution Explorer. Right click the project and select **Manager User Secrets**. This will automatically generate a **UserSecretsId** in your project’s `.csproj` configuration file, and it will open the associated `secrets.json` file. Suppose you must include a Client ID and Secret in order to access a private API. Rather than including them directly in your C# source code, you could add them to `secrets.json` as follows: ``` { "ClientId": "myclientid", "ClientSecret": "00aaaa0a-00aa-00aa-00aa-00aaaa0000aa" } ``` ## Access Secrets in Startup To access the data stored in the Secret Manager, simply use the Configuration API available in .NET Core. Keys are often needed as part of a project’s Startup routine, so open your project’s `Startup.cs` or `Program.cs` for project in NET6 or above. If you are not already using it, go ahead an inject an instance of the Configuration provider interface into the Startup constructor. ``` public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // ... } ``` Now, you can use the instance in either the `ConfigureServices()` method or the `Configure()`method of `Startup.cs` or `Program.cs` using the following syntax. ``` var value = Configuration["key"]; ``` For example, to access the values for the two keys in the Secret Manager, `ClientId` and `ClientSecret`, you might do something similar to the following. ``` string Id = Configuration["ClientId"]; //myclientid string Secret = Configuration["ClientSecret"]; //00aaaa0a-00aa-00aa-00aa-00aaaa0000aa ``` The `Id` variable will now hold the value associated with the `ClientId` key from `secrets.json`**.** In other words, `Id` is a string variable with value `myclientid`. The string variable `Secret` has a value of `00aaaa0a-00aa-00aa-00aa-00aaaa0000aa`. ## Access Secrets in Blazor and Razor Pages To use the data stored in Secret Manager in a Razor page, simply inject an instance of `IConfiguration` into the page. ``` @inject IConfiguration Configuration ``` Now, you can access your secrets by using Razor syntax and the Configuration API. For example, the following would display the `ClientSecret`. ``` Your secret is @Configuration["ClientSecret"] ``` ## Wrap up In conclusion, in this post we saw how to keep secrets out of source code using the specific function available in Visual Studio 2019 or above. **Categories:** .NET Core, .NET General, .NET5, .NET6, .NET7, ASP.NET, Blazor, Visual Studio **Tags:** aspnet-core, blazor, netcore, visual-studio, visualstudio-2022 --- ### [Agile methodology handbook](https://puresourcecode.com/dotnet/net-core/agile-methodology-handbook/) **Published:** June 17, 2022 **Author:** Enrico **Excerpt:** This is a quick announcement about a repository I have just started about Agile methodology handbook available on GitHub Pages **Content:** This is a quick announcement about a repository I have just started about [Agile methodology handbook](https://agile.puresourcecode.com/). I have created a repository in [GitHub](https://github.com/erossini/agile) for it and everybody can collaborate. To create this documentation, I’m using Docsify and you can read more about it on [my post](https://puresourcecode.com/tools/create-documentation-with-docsify-and-github-pages/). Google Trends data shows interest in terms like *agile* and *agile certification* has remained on a steady upward track worldwide since 2004. Agile has also been the focus of recent articles in publications as diverse as Forbes, CIO, Global Healthcare and Harvard Business Review exploring its applicability across functions from marketing to human resources. ![Search volume of 'Agile' via Google Trends - Agile methodology handbook](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-12.png?resize=640%2C283&ssl=1)Search volume of ‘Agile’ via Google Trends My intention is to create a handbook to help and support people in using Agile and its implementations. **Categories:** .NET Core --- ### [ScrollTabs component for Blazor](https://puresourcecode.com/dotnet/csharp/scrolltabs-component-for-blazor/) **Published:** November 7, 2022 **Author:** Enrico **Excerpt:** Do you have to display a lot of tabs in your page, but the look and feel is no what you want. Here you have a ScrollTabs component for Blazor **Content:** In this new post, I introduce to you the new ScrollTabs component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). This is quite a nice component for showing a lot of tabs. This is a new implementation of my other component [Tabs control for Blazor](https://puresourcecode.com/dotnet/blazor/tabs-control-for-blazor/). ![An example of the ScrollTabs component for Blazor in action](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/200393605-b8c6472e-b058-4caa-8710-ccac7916ad90.gif?w=640&ssl=1)An example of the ScrollTabs component for Blazor in action The source code of this component is available on [GitHub](https://github.com/erossini/BlazorScrollTabs). ## Scenario So, think about the following UI problem. In a page of your application, you want to display few tabs. To create them, you use Bootstrap. For example, you can add tabs with this code ``` Home Profile Contact ... ... ... ``` And the result is exactly what you think now. CodePen Embed Fallback This is perfectly fine if the title in each tab is short, and the tabs are visible in the mobile version of the website. However, if your tabs don’t have a short title and there are a lot of them, the mobile version is very painful and difficult to manage. How to display nicely the tabs? ## ScrollTabs is the answer For this reason, I created this ScrollTabs component for Blazor: you have a flexible component to display a lot of tabs without using a scroll bar that it is ugly but 2 simple arrows on both ends of the bar. ### How to add the ScrollTabs to your project First, if your project is a Blazor WebAssembly, open the *index.html* and add the following lines in the header of the page ``` ``` Then, before the `BODY` tag closed, add the following lines: ``` ``` An important thing to remember is that this component requires jQuery and Bootstrap. You can install those libraries in your project from **Visual Studio** > **Client-side library**. Finally, in the `_Imports.razor` add ``` @using PSC.Blazor.Components.ScrollTabs ``` ### Use the ScrollTabs In your `Razor` page, you can call the component like that ``` Content Tab 1 This is the content for the Tab 1. It is enabled. Content Tab 2 This is the content for the Tab 2. It is enabled. ``` ``` @code { public async Task OnTabChanged(Tab tab) { Console.WriteLine($"Tab value: {tab.Value} - Tab text: {tab.Text}"); } } ``` ### Themes Embedded in the components, there are 3 theme options: - Light - Dark - None You can use `None` to use your custom implementation. ## Add your style Very often, you have to add your style and then you have to create the CSS for it. The component has the property `CSSClass`: with this property you can set your CSS following this code: ``` /* Set the content of the arrows to nothing, we will use a background image of our own. */ .style1 .scroll_tab_left_button::before { content: ""; padding: 0px; } .style1 .scroll_tab_right_button::before { content: ""; padding: 0px; } /* Set up our background image (i.e. toolbar for example) behind the tab-set */ .style1 div.scroll_tab_inner { height: 40px; background: url(../images/toolbar-bg.jpg) repeat-x; } /* Setup the appearance of each individual TAB */ .style1 div.scroll_tab_inner span { padding-left: 20px; padding-right: 20px; line-height: 40px; font-size: 14px; background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -25px -40px; color: #FFFFFF; cursor: pointer; } /* Style the FIRST tab differently from the rest, inherits from above */ .style1 div.scroll_tab_inner span.scroll_tab_first { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -40px -40px; } /* Style the hover state for each tab */ .style1 div.scroll_tab_inner span.scroll_tab_over { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -25px 0px !important; } /* Style the hover state for the first tab (Using MULTI-CLASS selectors may not work right in older browsers) */ .style1 div.scroll_tab_inner span.scroll_tab_first.scroll_tab_over { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -40px 0px !important; } /* Style the left of the tab set if the arrows are hidden (the are is wide enough to show ALL the tabs */ .style1 div.scroll_tab_inner span.scroll_tab_left_finisher { padding: 0px; width: 10px; background: transparent url(../images/scroll_tab_bg.jpg) no-repeat 0px -160px; } /* AND HOVER STATE */ .style1 div.scroll_tab_inner span.scroll_tab_left_finisher.scroll_tab_over { background-position: 0px -120px !important; } /* AND SELECTED STATE */ .style1 div.scroll_tab_inner span.scroll_tab_left_finisher.tab_selected { background-position: 0px -200px; } /* Style the right of the tab set if the arrows are hidden (the are is wide enough to show ALL the tabs */ .style1 div.scroll_tab_inner span.scroll_tab_right_finisher { padding: 0px; width: 10px; background: transparent url(../images/scroll_tab_bg.jpg) no-repeat right -160px; } /* AND HOVER STATE */ .style1div.scroll_tab_inner span.scroll_tab_right_finisher.scroll_tab_over { background-position: right -120px !important; } /* AND SELECTED STATE */ .style1div.scroll_tab_inner span.scroll_tab_right_finisher.tab_selected { background-position: right -200px; } /* Style left scrolling button */ .style1 .scroll_tab_left_button { height: 40px; background: transparent url(../images/scroll_tab_bg.jpg) no-repeat 0px -40px; } /* Style left scrolling button HOVER */ .style1 .scroll_tab_left_button.scroll_arrow_over { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat 0px 0px; } /* Style left scrolling button DISABLED */ .style1 .scroll_tab_left_button.scroll_arrow_disabled { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat 0px -80px; } /* Style right scrolling button */ .style1 .scroll_tab_right_button { height: 40px; background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -474px -40px; } /* Style right scrolling button HOVER */ .style1 .scroll_tab_right_button.scroll_arrow_over { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -474px 0px; } /* Style right scrolling button DISABLED */ .style1 .scroll_tab_right_button.scroll_arrow_disabled { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -474px -80px; } /* Style SELECTED (depressed) tab */ .style1 div.scroll_tab_inner span.tab_selected { background: transparent url(../images/scroll_tab_bg.jpg) no-repeat -25px -80px; } /* Style SELECTED (depressed) if it's the FIRST tab */ .style1 div.scroll_tab_inner span.tab_selected.scroll_tab_first { background-position: -40px -200px; } ``` ## Wrap up In conclusion, I hope that the ScrollTabs component for Blazor will be useful in your project. If you have any questions or comment, please write it in the [Forum](https://puresourcecode.com/forum). **Categories:** .NET6, Blazor, C# **Tags:** blazor, blazor-component, blazor-webassembly --- ### [How to create String Enums](https://puresourcecode.com/dotnet/net-core/how-to-create-string-enums/) **Published:** November 12, 2022 **Author:** Enrico **Excerpt:** More often than not, I try to have an enum but for strings: in this post, I show you how to create String Enums in NET6 and C#. **Content:** More often than not, I try to have an `enum` but for strings: in this post, I show you how to create String Enums in [NET6](https://puresourcecode.com/tag/net6/) and C# but it is straightforward to use it in another version of the framework. ## What is an enum? Enumeration (or enum) is a value data type in C#. It is mainly used to assign the names or string values to integral constants, that make a program easy to read and maintain. For example, the 4 suits in a deck of playing cards may be 4 enumerators named Club, Diamond, Heart, and Spade, belonging to an enumerated type named Suit. Other examples include natural enumerated types (like the planets, days of the week, colors, directions, etc.). The main objective of `enum` is to define our own data types (Enumerated Data Types). Enumeration is declared using `enum` keyword directly inside a namespace, class, or structure. Consider the below code for the enum. Here enum with name `month` is created and its data members are the name of months like jan, feb, mar, apr, may. Now let’s try to print the default integer values of these enums. An explicit cast is required to convert from `enum` type to an integral type. ``` public enum Cards { Club, Diamond, Heart, Spade } ``` Sometimes, you can add `Description` as annotation and then use a function to give the text (see my [NuGet](https://www.nuget.org/packages/PSC.Extensions/) package [PSC.Extensions](https://github.com/erossini/PSC.Extensions) where I have few functions for it). Then, if you want to define a variable with this `enum`, you use it like this ``` Cards myCard = Cards.Club; ``` So, it is easy to assign a correct value. The problem is there is not native `enum` for strings. So, I found a nice workaround. ## Implement a String Enum First, I want to have something like the real `enum` that I can easily use as a type. Then, I want to access the values in a similar way. So, I implemented for my [ChartJs Blazor Component](https://puresourcecode.com/dotnet/blazor/blazor-component-for-chartjs/), the following solution: ``` public class Align { private Align(string value) { Value = value; } public string Value { get; private set; } public static Align Start { get { return new Align("start"); } } public static Align Center { get { return new Align("center"); } } public static Align End { get { return new Align("end"); } } } ``` If you want to use it, it is like an `enum`. The only difference is that you have to call `Value` to know the exact value. For example: ``` Align align = Align.Start; var value = align.Value; ``` I think we can cope with it. The advantage is to access easily to the values and easy to maintain. ## Wrap up In conclusion, this is how to create String Enums in C#. Please give me your feedback below or in the [Forum](https://puresourcecode.com/forum/). **Categories:** .NET, .NET Core, .NET6 **Tags:** enums, net6, netcore, netstandard --- ### [Is Star Wars Eclipse coming?](https://puresourcecode.com/news/is-star-wars-eclipse-coming/) **Published:** November 11, 2022 **Author:** Enrico **Excerpt:** If you are a Star Wars fan, the first question I had in my mind was: is Star Wars Eclipse coming out this year? The trailer is quite stunning. **Content:** If you are a Star Wars fan, the first question I had in my mind was: is [Star Wars Eclipse](https://www.starwarseclipse.com/) coming out this year? The trailer is quite stunning. I don’t talk a lot about games only for [Star Wars](https://puresourcecode.com/games/star-wars-squadrons-is-an-intricate-dogfighter/) 😉 ## What is Star Wars Eclipse? Star Wars Eclipse is an upcoming action-adventure video game developed by Quantic Dream and set during the High Republic Era. Featuring a cast of multiple playable characters, it is planned to allow players to change the events of the story based on their decisions in-game. Announced on December 9, 2021, Eclipse currently has no release date. Star Wars: Eclipse is one of the upcoming Star Wars games on the horizon that is coming from Detroit: Become Human and Heavy Rain developer, Quantic Dream. First revealed at The Game Awards 2021, Star Wars: Eclipse is set to be an action-adventure game featuring multiple characters and a branching narrative. Set in the High Republic era of the Star Wars galaxy, the project is in early development, but there are plenty of details to tuck into from the initial reveal. ## Star Wars Eclipse release date While described as in “early development” at the Game Awards, reports emerged towards the end of 2021 that suggested Star Wars Eclipse is apparently at least three to four years away from launch. Quantic Dream went on to respond to the reports to reaffirm that there have been no delays since no launch date has been confirmed. There’s little to go on beyond that at present, but, given Quantic Dream’s past projects, we expect Star Wars Eclipse to feature on PC, PS5, and [Xbox Series X](https://puresourcecode.com/tag/xbox-series-s/) at a minimum, with the developer promising a game that “can be experienced in many ways, and puts the destinies of multiple playable characters in your hands”. **Categories:** Games, News **Tags:** games, xbox, xbox-series-s --- ### [Embed GitHub Gists on WordPress](https://puresourcecode.com/tools/embed-github-gists-on-wordpress/) **Published:** November 11, 2022 **Author:** Enrico **Excerpt:** With the code in this post, we can embed Github Gists on WordPress. Gist is an easy method to share code snippets. **Content:** With the code in this post, we can embed [GitHub Gists](https://gist.github.com/) on WordPress. Gist is an easy method to share code snippets or excerpts of data with others. Gist is owned by [GitHub](https://puresourcecode.com/tag/github/) and used by millions of developers across the world. ![Home Page of Gist for creating a new code snippet - Embed GitHub Gists on WordPress](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-7.png?resize=640%2C493&ssl=1)Home Page of Gist for creating a new code snippet Today, I’m excited to share this very handy code snippet which allow you to embed a GitHub gist on your blog, simply by pasting the gist url. ## Add the function Paste the following code into your functions.php file. Once done, simply paste the URL of a Github gist into a post or page. The gist will be automatically embedded in your WordPress blog. ``` **Categories:** Tools **Tags:** gist, gist-github, github, wordpress --- ### [How to make menus with CSS](https://puresourcecode.com/programming-languages/htmlcss/how-to-make-menus-with-css/) **Published:** November 10, 2022 **Author:** Enrico **Excerpt:** I will show how to make menus with CSS without using JavaScript: dropdown menus, drawers, and even a mobile hamburger menu **Content:** In this new post, I will show how to make menus with CSS without using **[JavaScript](https://puresourcecode.com/tag/javascript/)**. There were **dropdown menus**, **drawers**, and even a **mobile hamburger menu** on one of the mocks. After a little digging, I found examples for all of them! While there were some tricks involved that I would have never thought of, the tricks themselves were simple. I took those new tricks and used them to finish the coding challenge. So, here’s how I managed to recreate all of those menus with CSS alone. ## The Dropdown Menu First, the menu I want to talk about is the dropdown menu. You’ve probably seen this on any e-commerce site or anything with lots of categories and navigation. There’s a header up top with a list of categories, and when you mouse over one, a new menu appears with more related items. ![Example of a classc dropdown menu - How to make menus with CSS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-3.png?resize=640%2C187&ssl=1)Example of a classc dropdown menu Here’s an example: ``` .dropdown__header * { padding: 0; margin: 0; } .dropdown__header { display: flex; align-items: center; background: lightblue; } .dropdown__header strong { margin-left: 5px; margin-right: auto; font-size: 1.6rem; } .dropdown__header .dropdown__categories, .dropdown__header .dropdown__menu { display: flex; list-style: none; padding: 0; margin: 0; } .dropdown__header li { padding: 10px; position: relative; } .dropdown__header li:hover { background: #d2f2fc; } .dropdown__header .dropdown__category .dropdown__menu { display: none; position: absolute; background: #ebfaff; width: 200px; top: 40px; right: 0; } .dropdown__header .dropdown__category:hover .dropdown__menu { display: block; } ``` And this is the HTML for the menu ``` Logo Cat 1 Cat 1 Thing Cat 1 Thing Cat 1 Thing Cat 1 Thing Cat 2 Cat 2 Thing Cat 2 Thing Cat 2 Thing Cat 2 Thing CSS Only Dropdown! Hover over the Categories above ``` CodePen Embed Fallback ### What makes this work? There’s three main concepts here that make this possible: **Showing/Hiding with Psuedoselectors**, **absolute positioning**, and **mindful HTML structure**. #### Showing/Hiding with Pseudoselectors I think most people are comfortable showing/hiding things with `display: none` and `display: block`. The real trick here is the selectors. We’re targetting `.dropdown__menu`, but notice the nesting structure. We’re looking for a `.dropdown__menu` class that’s within a `.dropdown__category` class. Then we can apply the `:hover` pseudoselector to the parent, which means we’re targetting the menu based on a hover over the parent. ``` .dropdown__header .dropdown__category .dropdown__menu { display: none; } .dropdown__header .dropdown__category:hover .dropdown__menu { display: block; } ``` #### Mindful HTML Structure Are you wondering why our menu stays open even when you move your mouse into the menu? Going along with our nesting explanation above, note that our `.dropdown__menu` is contained within `.dropdown__category`: ``` Cat 1 Cat 1 Thing Cat 1 Thing Cat 1 Thing Cat 1 Thing ``` This means that although we’re no longer over the words “Cat 1”, the mouse is still within the `.dropdown__category` list item, keeping the `:hover` selector trigger active. **Note**: The menu and category elements must be adjacent to each other so that your mouse stays within one or the other—you can’t position the menu off by itself somewhere and be able to mouse into it. More on that when we get to positioning. #### Absolute Positioning We position the hovered menu next to the category in the header using `position: absolute;`. W3 Schools has a [great breakdown](https://www.w3schools.com/css/css_positioning.asp) of CSS positioning, but here’s the short version: When you set positioning, items can be moved around with `top`, `bottom`, `left`, and `right` options. - **Static**: The default. The element behaves normally, and is unaffected by top/bottom/left/right. - **Fixed**: The element is positioned relative to the viewport. Eg: `top: 0;` would be the top of the viewer’s screen. - **Relative**: The element is positioned based on where it would be normally. So `top: 0` wouldn’t move it at all, and `top: 10px` would push it down 10 pixels. - **Sticky**: This is a newer, trickier positioning. The element stays where it is, but when the user scrolls the page, before the element moves out of view, it will still stick to whatever top/bottom/left/right position is set. And lastly, there’s **Absolute** positioning. This is the most complicated. The behavior depends on whether this element is within an element that has some kind of positioning set. **If it has a positioned ancestor, it positions relative to that ancestor element. If not, it is positioned relative to the whole page.** In the case of our dropdown menus, we make the `li` tag that contains the menu `relative`. Then within it we can use `position: absolute;` to position the expanded menu around our category title. ``` .dropdown__header li { position: relative; } .dropdown__header .dropdown__category .dropdown__menu { position: absolute; top: 40px; right: 0; } ``` Here we right-align the menu, and bump it down just far enough so that it’s at the bottom of “Cat 1” under our mouse. ## Nested Dropdown Menus Using the same techniques, we can create a more complicated dropdown menu: ![CSS nested dropdown - How to make menus with CSS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-4.png?resize=640%2C155&ssl=1)CSS nested dropdown ``` .nested-dropdown__header * { padding: 0; margin: 0; } .nested-dropdown__header { display: flex; align-items: center; background: lightblue; } .nested-dropdown__header strong { margin-left: 5px; margin-right: auto; font-size: 1.6rem; } .nested-dropdown__header ul { display: flex; list-style: none; padding: 0; margin: 0; } .nested-dropdown__header li { padding: 10px; position: relative; } .nested-dropdown__header li:hover { background: #d2f2fc; } .nested-dropdown__header .nested-dropdown__menu, .nested-dropdown__header .nested-dropdown__submenu { display: none; position: absolute; background: #ebfaff; width: 150px; top: 50px; right: 0; } .nested-dropdown__header .nested-dropdown__category:hover .nested-dropdown__menu, .nested-dropdown__header .nested-dropdown__subcategory:hover .nested-dropdown__submenu { display: block; } .nested-dropdown__header .nested-dropdown__submenu { top: 0; right: 100%; } ``` And this is the HTML ``` Logo Cat 1 Sub Cat 1 Sub Cat 1 Thing Sub Cat 1 Thing Sub Cat 1 Thing Sub Cat 1 Thing Sub Cat 2 Sub Cat 2 Thing Sub Cat 2 Thing Sub Cat 2 Thing Cat 1 Thing Cat 1 Thing CSS Only NESTED Dropdown! Hover over 'Cat 1' above, Then check out the Sub Categories! ``` CodePen Embed Fallback There’s only two caveats to this nested menu. The first is that it doesn’t work well on mobile devices. This type of menu requires a lot of real-estate and hovers don’t translate to touchscreens very well. This isn’t a fault of CSS, though. Most sites will switch to a different style of menu on smaller devices, regardless of CSS vs JS. The second is that if your user wants to jump from a menu to a nested menu, she may take the shortest path and unintentionally move her mouse cursor out of the current element. Deeply nested menus are a little perilous for the user, so use with caution. ## Content Drawers Creating drawers with CSS alone seemed impossible. Hover effects don’t work, because when you open a drawer, you expect it to stay open. However, I learned that it *is* possible with some very clever uses of HTML input elements and CSS selectors. ![CSS Content drawer example - How to make menus with CSS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-5.png?resize=474%2C429&ssl=1)CSS Content drawer example This example has quite a bit of CSS, but don’t fret, I promise to explain the important bits: ``` .drawer { position: relative; box-sizing: border-box; width: 300px; } .drawer input { width: 100%; height: 60px; position: absolute; opacity: 0; top: 0; left: 0; margin: 0; z-index: 3; cursor: pointer; } .drawer__name { display: flex; justify-content: space-between; border: 1px solid black; padding: 10px; background: lightblue; } .drawer__content { border: 1px solid black; box-sizing: border-box; width: 100%; margin: 0; height: 0; opacity: 0; pointer-events: none; list-style: none; transition: padding 0.2s; } .drawer input:checked ~ .drawer__content { height: 100%; opacity: 1; pointer-events: initial; padding: 10px 0; } .drawer__arrow { font-weight: 900; font-size: 1.2rem; transition: transform 0.2s; } .drawer input:checked ~ .drawer__name > .drawer__arrow { transform: rotateZ(90deg); } .drawer__content li { padding: 10px; cursor: pointer; } .drawer__content li:hover { color: rebeccapurple; text-decoration: underline; } ``` And this is the HTML code ``` Bananas > Bruised Green Perfect Apples > Mealy Crisp Green Oranges > Bitter Sour Sweet ``` CodePen Embed Fallback It seems like a lot of code is required to make this happen, but there’s just a few key pieces to this puzzle that make it possible. ### What makes this work? The main trick here really did blow my mind. The secret is **HTML checkbox elements**. **Note:** I would have never thought of this in a million years. When I saw an example of this on Codepen I experienced the same feelings I had when seeing an awesome magic trick explained; I felt amazement of the ingenuity involved, and also anger at being duped by such a simple trick that I should have been able to figure out for myself. If we position the checkbox over the drawer and stretch it to the same size as the drawer, then target siblings on a `:checked` state, we can eliminate the need for JavaScript to trigger the open/close events. The CSS sibling selector is `~`, meaning target any neighbor elements coming after this element. It’s important to note that the **checkbox must come first in the HTML**. You can select sibling/neighbour elements that come after, but not before. We also use `~` and not `+` because `+` targets only the first adjacent sibling element, whereas `~` will give you any following sibling. The other important piece to this puzzle is the transition. Notice we didn’t use `display` here to show and hide the drawer. If you do, you won’t be able to use CSS transitions to animate. **You can’t animate an element that’s hidden with `display: none`** Instead, we use a combo of `height`, `padding`, `opacity`, and `pointer-events`. (That seems like a lot, but hear me out). Here’s the main code that makes this possible for reference: CodePen Embed Fallback First we squash the `.drawer__content` with `height: 0`, and hide it with `opacity: 0`. However, because it’s still on the page, the user would be able to click the things within, even though the content isn’t visible. The solution is to disable mouse interaction with `pointer-events: none`. This allows us to animate the content while not letting the use see or interact with it. Finally, we use `padding` as our animation. Animating the `height` causes some weird behavior, but animating the padding allows some for some subtle animation while keeping the drawer itself very responsive. It’s possible to make drawers with just HTML alone using the `details` element. You lose out on the ability to animate with CSS, though. ![Drawer example](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-6.png?resize=467%2C351&ssl=1)Drawer example Here’s a quick example. None of the CSS is necessary for the drawer behavior: ``` details { background: rebeccapurple; color: #ddd; width: 300px; border-radius: 4px; margin-bottom: 5px; } summary { padding: 10px; } summary:focus { outline: none; } details ul { background: #ddd; margin: 0; color: black; padding: 10px; border-radius: 0 0 4px 4px; } details li { margin-left: 30px; margin-top: 10px; } ``` ``` Drawer 1 Thing 1 Thing 2 Thing 3 Thing 4 Drawer 2 Thing 1 Thing 2 Thing 3 Thing 4 ``` CodePen Embed Fallback ## CSS Only Hamburger Menus Now for the coup de grâce, CSS-only Hamburger menus! What’s a hamburger menu? It gets its name from the three stacked dashes that indicate “hey this is a menu”. I guess someone was awful hungry for three lines to look like a hamburger, but I digress. You may have some ideas on how to pull this off in your head after seeing the previous examples. Let’s take a look (Warning, incoming a ton of CSS): ``` .hamburger-menu__wrapper { min-height: 400px; position: relative; text-align: center; } .hamburger-menu { height: 100%; width: 300px; position: absolute; top: 0; left: 0; } .hamburger-menu__button { width: 40px; height: 40px; border: 2px solid #777; border-radius: 5px; display: flex; flex-direction: column; justify-content: center; position: relative; z-index: 3; } .hamburger-menu__button span { line-height: 8px; text-align: center; font-size: 1.6rem; font-weight: 400; } .hamburger-menu__button span:last-child { padding-bottom: 5px; } .hamburger-menu__wrapper { position: relative; height: 100%; overflow: hidden; } .hamburger-menu__wrapper input[type='checkbox'] { width: 40px; height: 40px; outline: 3px solid red; opacity: 0; position: absolute; top: 6px; left: 6px; z-index: 4; cursor: pointer; } .hamburger-menu__wrapper input:checked ~ .hamburger-menu__button { background: #d2f2fc; } .hamburger-menu__wrapper input:checked ~ .hamburger-menu__slider { transform: none; } .hamburger-menu__slider { position: absolute; top: 0; left: 0; width: 300px; height: 100%; background-color: lightblue; z-index: 2; transform: translateX(-310px); transition: transform 0.3s; text-align: left; } .hamburger-menu__item { padding: 10px 0 10px 10px; } .hamburger-menu__item:hover { background-color: #d2f2fc; } .hamburger-menu__item:first-of-type { margin-top: 60px; } ``` ``` — — — Thing 1 Thing 2 Thing 3 Thing 4 Hamburger!! Click the Button to toggle the menu ``` CodePen Embed Fallback Take ***that***, bootstrap. 🤠 ### What makes it work? A lot of the CSS above is for creating that silly hamburger style button. I literally stacked dashes and put a border around them, but you can use an icon if you like. As for the menu, it uses **absolute positioning**, the **input checkbox trick**, as well as a new trick, which is using **transform** to slide the drawer in and out. Here’s the code that makes the menu slide: ``` .hamburger-menu__wrapper { position: relative; height: 100%; overflow: hidden; } .hamburger-menu__wrapper input:checked ~ .hamburger-menu__slider { transform: none; } .hamburger-menu__slider { position: absolute; top: 0; left: 0; width: 300px; height: 100%; z-index: 2; transform: translateX(-310px); transition: transform 0.3s; } ``` We style the menu to sit on top of the page, then add in `transform: translateX(-310px)` to pull the menu to the left so it’s no longer visible. When the `input` is checked, we remove that `transform` and the transition animates the drawer sliding into view. There’s a ton of other neat things you can do with the `transform` property. Now, we used it above on our drawers to turn our arrow to point downward when the drawer is open, for example. We also use `overflow: hidden` on the wrapper to make sure our menu isn’t visible when the menu is closed. We also use `z-index` here to make sure things stack on top of each other. Absolute positioning breaks items out of normal HTML flow, so making sure the menu is on top is crucial. Even more critical, we apply a higher z-index to the checkbox to make sure the user can always open and close the menu. We applied `z-index` to our drawer checkboxes as well above. **Categories:** HTML\CSS **Tags:** css, hamburger, html, menu --- ### [How to add badges to a GitHub repository](https://puresourcecode.com/tools/how-to-add-badges-to-a-github-repository/) **Published:** November 9, 2022 **Author:** Enrico **Content:** In this article, I will show how to add badges to a GitHub repository using [shields.io](https://shields.io/). This application is a web service that can be used to generate concise, consistent, and legible badges in SVG. Adding badges to the readme file of GitHub repositories is a common task for almost every new repository. These badges help in increasing the readability of the readme file because they provide some KPIs or metrics about the repositories. As a result, readers get a clear idea of the repository very quickly by scanning the attached badges. The badges that can be added to the repositories cover several topics and areas, starting from the license of the project to the open issues count for the project. ## License You can generate a license badge for your GitHub repository by using [the following link](https://shields.io/category/license). From here, you can select where your repository is, for example GitHub. ![Shields.io Licence badge - How to add badges to a GitHub repository](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image.png?resize=640%2C628&ssl=1)Shields.io Licence badge Then, open the GitHub link and fill your repository information, as shown in the image below. In this example, I type my [GitHub account](https://github.com/erossini) and the repository from where I want to display the licence that is [BlazorChartJs](https://github.com/erossini/BlazorChartjs) (see more about this Blazor component in [this post](https://puresourcecode.com/dotnet/blazor/blazor-component-for-chartjs/)). ![Shield.io GitHub Licence - How to add badges to a GitHub repository](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-1.png?resize=640%2C441&ssl=1)Shield.io GitHub Licence ## Version In case your repository is for a NPM or other libraries, you can add a badge with the version of the package by using [the following link](https://shields.io/category/version). The most interesting options are: - `Github release`: We can generate a version badge based on the repository releases. - `Gem`: We can create a version badge for a given Ruby gem. The images below show how we can customize these badges for both options. ![Shields.io GitHub release - How to add badges to a GitHub repository](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/11/image-2.png?resize=640%2C490&ssl=1)Shields.io GitHub release [fury.io](https://badge.fury.io/) is an alternative for generating the version badges for your repository. ## Build Status Build status badges can tell us the last build status of the project. There are several options in this category. Depending on your project type and the tools used for CI/CD pipelines, you can choose one of the available options. Some of the most used badges are `Github workfolw Status`, `Jenkins`, `Tracis ci` , and `CircleCI` . Badges for all these CI/CD tools can be added from [this link](https://shields.io/category/build). ## Conclusion In conclusion, this is how to add badges to a GitHub repository. Badges can improve the readability of a GitHub repository because they provide the users with a quick way to collect metrics about the repository. The aforementioned badges are not the only ones that you can include in your repositories. There are a lot of other badges that could be more applicable to your project, such as `Github issues, Coveralls github, Jenkins Coverage, Docker Build Status`, and many more. You can check all of these out on [shields.io](https://shields.io/category/size) or [badgen.net](https://badgen.net/). **Categories:** Tools **Tags:** badge, version --- ### [Tim Berners-Lee wants us to ignore Web3](https://puresourcecode.com/news/tim-berners-lee-wants-us-to-ignore-web3/) **Published:** November 7, 2022 **Author:** Enrico **Excerpt:** The creator of the web Tim Berners-Lee wants us to ignore Web3 and isn’t sold on crypto visionaries’ plan for its future. We should ignore it **Content:** The creator of the web isn’t sold on crypto visionaries’ plan for its future and says we should “ignore” it. Tim Berners-Lee, the British computer scientist credited with inventing the World Wide Web in 1989, said Friday that he doesn’t view blockchain as a viable solution for building the next iteration of the internet. He has his own web decentralization project called Solid. “It’s important to clarify in order to discuss the impacts of new technology,” said Berners-Lee, speaking onstage at the Web Summit event in Lisbon. “You have to understand what the terms mean that we’re discussing actually mean, beyond the buzzwords.” “It’s a real shame in fact that the actual Web3 name was taken by Ethereum folks for the stuff that they’re doing with blockchain. In fact, Web3 is not the web at all.” Web3 is a nebulous term in the tech world used to describe a hypothetical future version of the internet that’s more decentralized than it is today and not dominated by a handful of powerful players such as Amazon, Microsoft and Google. It involves a few technologies, including blockchain, cryptocurrencies and nonfungible tokens. While breaking our personal data out of Big Tech’s clutches is an ambition shared by Berners-Lee, he’s not convinced blockchain, the distributed ledger technology that underpins cryptocurrencies like bitcoin, will be the solution. “Blockchain protocols may be good for some things but they’re not good for Solid,” a web decentralization project led by Berners-Lee, he said. “They’re too slow too expensive and too public. Personal data stores have to be fast, cheap and private.” “Ignore the Web3 stuff, random Web3 that was built on blockchain,” he added. “We’re not using that for Solid.” Berners-Lee said people too often conflate Web3 with “Web 3.0,” his own proposal for reshaping the internet. His new startup, Inrupt, aims to give users control of their own data, including how it’s accessed and stored. The company raised $30 million in a funding round in December, [TechCrunch reported](https://techcrunch.com/2021/12/09/tim-berners-lee-inrupt-fundraise/). Berners-Lee says that our personal data is siloed by a handful of Big Tech platforms, like Google and Facebook, that use it to “lock us into their platforms.” “The result was a big data race where the winner was the one corporation that controlled the most data and the losers were everybody else,” he said. His new startup aims to address this through three ways: A global “single sign-on” feature that lets anyone log in from anywhere. Login IDs that allow users to share their data with others. A “common universal API,” or application programming interface, that lets apps pull data from any source. Berners-Lee’s not the only notable tech figure with doubts about Web3. The movement has been a punching bag for some leaders in Silicon Valley, like Twitter co-founder Jack Dorsey and Tesla CEO Elon Musk. Critics say it’s prone to the same issues that come with cryptocurrencies, like fraud and security flaws. **Categories:** News, Other **Tags:** berners-lee, internet, web3 --- ### [Responsive table layout with only CSS](https://puresourcecode.com/programming-languages/htmlcss/responsive-table-layout-with-only-css/) **Published:** October 26, 2022 **Author:** Enrico **Excerpt:** I want to show you how to create a fully responsive table layout with only CSS that helps you to display your tables on every screen. **Content:** Very different type of post because I want to show you how to create a fully responsive table layout with only CSS. This will help you to display your tables, large or small, on every screen in a pleasant way. ## The issue First, let’s say I’m viewing Wikipedia on my iPhone, I’m looking through the episode list for *[Star Trek: The Next Generation](https://en.wikipedia.org/wiki/Star_Trek:_The_Next_Generation)*, and the table has a lot of columns and data. There ends up being a lot of back-and-forth side swiping, device flipping, and general annoyance as I muddle through that user experience. So, it’s an issue that exists broadly across the web, even though there are several responsive table solutions available. Here a demo of the solution I found. ![Demo Responsive table layout with only CSS](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/10/Responsive-table-layout-with-only-CSS.gif?resize=640%2C393&ssl=1) ## Create a basic table So, we’ll create the same table above with some generic HTML ``` Statement Summary Account Due Date Amount Period Visa - 3412 04/01/2016 $1,190 03/01/2016 - 03/31/2016 ``` After that, our table has four columns. Let’s add some basic CSS selectors to better define the table layout: ``` table { border: 1px solid #ccc; border-collapse: collapse; margin: 0; padding: 0; table-layout: fixed; width: 100%; } table tr { background-color: #f8f8f8; border: 1px solid #ddd; padding: .35em; } table th, table td { padding: .625em; text-align: center; } ``` ## Responsive time First, we’ll add a `data-label` attribute to each data cell with a value that represents that column’s name. That will be used for labeling purposes in the responsive layout. ``` Visa - 3412 04/01/2016 $1,190 03/01/2016 - 03/31/2016 ``` Now, we can begin writing a CSS media query: ``` @media screen and (max-width: 600px) { table thead { border: none; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } table tr { border-bottom: 3px solid #ddd; display: block; } table td { border-bottom: 1px solid #ddd; display: block; text-align: right; } table td::before { content: attr(data-label); float: left; } } ``` In smaller viewports the `` and `` elements will display as block-level and not as table rows and cells. And the `::before` pseudo-element now serves as a label. So, here’s our table (flip your device screen between portrait and landscape view): CodePen Embed Fallback ## Wrap up In conclusion, this is a simple way to get a nice responsive table layout with only CSS in particular for mobile devices. For any question, please use the form below or create a post in the [Forum](https://puresourcecode.com/forum/). **Categories:** HTML\CSS **Tags:** css, html, mobile, responsive, table **Hashtags:** html, mobile, responsive --- ### [Create form dynamically with Blazor](https://puresourcecode.com/dotnet/blazor/create-form-dynamically-with-blazor/) **Published:** September 23, 2022 **Author:** Enrico **Excerpt:** I show you how to create form dynamically with Blazor reading the definition of the form from an API or a Json file. It is working quite well **Content:** In this post, I show you how to create form dynamically with Blazor without using `DataAnnotation` but only simple classes. My goal is to create a survey dynamically at run-time based on a `Json` file. I spent a lot of time to architect this code and I have created a component that allows you to create the form for the survey and validate the structure of the form and the result. Also, I added the opportunity to insert simple condition to display or not some options or content. For testing, I have created a website with the latest version available on [SurveyUI](https://survey.puresourcecode.com/). You have the source code of this example on [GitHub](https://github.com/erossini/BlazorDynamicForm). ## Shared classes First, I have to create a common class for all the components. I’m calling this class `Element` and the code is the following ``` public class Element { public virtual string ElementType { get; set; } public string Name { get; set; } public string Label { get; set; } } ``` Here, I have just defined the basic properties of each component. Now, I’m going to create a class for each of the component and in particular `TextInput` and `RadioButton`. ``` public class TextInput : Element { public override string ElementType { get => "TextInput"; } public string? PlaceHolder { get; set; } public string? Value { get; set; } } public class RadioButton : Element { public override string ElementType { get => "RadioButton"; } public Dictionary Options { get; set; } } ``` Then, I’m creating a new class to collect all the element and calling this class `Form` ``` public class Form { public List Elements { get; set; } = new List(); } ``` ## Read the Form from an API So, I want to read the structure of the form from a `Json` or in the case of this post from an API. For this reason, I’m creating a `Controller` to return a form. Also, I’m creating an API to read the result. ``` using BlazorDynamicForm.Shared; using Microsoft.AspNetCore.Mvc; namespace BlazorDynamicForm.Server.Controllers { [Route("[controller]")] [ApiController] public class FormController : ControllerBase { [HttpGet] public Form Get() { return new Form { Elements = new List { new TextInput { Name = "txtFName", Label = "First Name", PlaceHolder = "Enter your first name" }, new TextInput { Name = "txtLName", Label = "Last Name", PlaceHolder = "Enter your last name" }, new RadioButton { Name = "radGender", Label = "Gender", Options = new Dictionary { { "M", "Male" }, { "F", "Female" } } } } }; } [HttpPost] public string Submit([FromBody] Dictionary formValues) { return $"Hello {formValues["txtFName"]} {formValues["txtLName"]}"; } } } ``` ## In the Blazor Client To demonstrate the components which are the core of **Blazor** we are creating a corresponding component for each element. Component’s name is coming from their file name (it should start with Capital letter). They are a Html with a *@code* section which would have all the events, properties, logic and other things. We are creating a folder called *Components* at the same level of *Pages*. For this sample we would only create *TextInput.razor* and *RadioButton.razor*. ### TextInput.razor ``` @code { [Parameter] public string Name { get; set; } [Parameter] public string PlaceHolder { get; set; } [Parameter] public string Value { get; set; } } ``` ### Radiobutton.razor ``` @foreach (var option in @Options) { @option.Value } @code { [Parameter] public string Name { get; set; } [Parameter] public Dictionary Options { get; set; } } ``` ### The Razor page Now, I’m going to create a Razor page to display the form and I’m calling this page `DynamicForm.razor` ``` @page "/dynamicform" @using BlazorDynamicForm.Shared Dynamic form @if (form == null) { Loading... } else { @foreach(var element in form.Elements) { @element.Label @switch (element.ElementType) { case "TextInput": { if (!ElementValues.ContainsKey(element.Name)) ElementValues.Add(element.Name, (element as BlazorDynamicForm.Shared.TextInput).Value); break; } case "RadioButton": { BlazorDynamicForm.Shared.RadioButton rdb = element as BlazorDynamicForm.Shared.RadioButton; break; } default: { Unknow control break; } } } Submit @if (!string.IsNullOrEmpty(strForm)) { Form: @strForm } @if (!string.IsNullOrWhiteSpace(serverRequest)) { Request from server: @serverRequest } @if (!string.IsNullOrWhiteSpace(serverResponse)) { Response from server: @serverResponse } } @code { Form form; protected override async Task OnInitializedAsync() { var st = await Http.GetStringAsync("Form"); form = JsonConvert.DeserializeObject(st, settings: new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto }); } } ``` ## Running the solution After that, we can run the solution. The result is what you can see in the following picture ![Create form dynamically with Blazor in action](https://i0.wp.com/github.com/erossini/BlazorDynamicForm/blob/main/Screenshots/blazor-dynamicform.gif?w=640&ssl=1)Create form dynamically with Blazor in action ## SurveyUI Now, you have here a working code of a form generator based on an API or a Json file. This code is a starting point: I started with this example to create a proper component to generate very complex form for marketing research purposes. If you like to see my component SurveyUI in action, visit the website [SurveyUI](https://survey.puresourcecode.com/). Also, I have created a library in `.NET Standard 2.1` for evaluating expressions. You can install it from Nuget and its name is [PSC.Evaluator](https://www.nuget.org/packages/PSC.Evaluator/). My next post will be about this new library, but you already can start to use it. [PSC.Evaluator](https://www.nuget.org/packages/PSC.Evaluator/) is a mathematical expressions evaluator library written in C# and allows to evaluate mathematical, boolean, string and datetime expressions. With this library, I can evaluate expressions in the form for example to display or hide some options or questions in the form. If you have a look to the SurveyUI website, you find some examples. ### List of components In the SurveyUI component for Blazor, there are different kinds of components: - Basic components - Group of components - Custom components and components for marketing research - Group of elements #### Basic components - Textbox - Checkbox - Slider - Radiobutton - Dropdownlist - Comment - Boolean - ImagePicker - Upload file - HTML #### Group of components - Matrix (Single choice) - Matrix (Multiple choices) - Matrix Dynamic rows - Multiple Textbox #### Custom components and components for marketing research - NPS (Net Promoter Score) - Likert Skill - Multiselect - Semantic differential - Rating - Ranking #### Group of elements - Panel - Repeater - Page Let me know what you think about those libraries: if you have any questions about SurveyUI, add your comments on [this forum](https://puresourcecode.com/forum/survey-generator/). If you have questions about PSC.Evaluator, use [this forum](https://puresourcecode.com/forum/psc-evaluator/). **Categories:** Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly **Hashtags:** blazor, blazor-component --- ### [Manipulating CSV Files](https://puresourcecode.com/dotnet/net-core/manipulating-csv-files/) **Published:** September 21, 2022 **Author:** Enrico **Excerpt:** How manipulating CSV files in the internet era? Comma Separated Files (CSV) are text files that contain multiple records with more elements **Content:** How manipulating CSV files in the internet era? **Comma Separated Files** (**CSV**) are text files that contain multiple records (rows), each with one or more elements (columns) separated by a comma character. Actually, the elements can be separated by any type of delimiter, not only a comma. For instance, another common file format is the Tab Separated Value (TSV) file where every element is separated by a tab character. So, CSV files present a *unique set of opportunities for sharing large quantities of data* as they’re dense and contain little of the wasted content that’s commonly found in JSON or XML files. They also compress rather nicely, which lowers bandwidth uses. ## The basic example As stated above, this article will be all about reading and writing movie data formatted in various CSV formats. The following class code represents the data: ``` public class Movie { public string Name { get; set; } = ""; public string Director { get; set; } = ""; public DateTime DateReleased { get; set; } public decimal BoxOfficeGross { get; set; } = 0.0m; } public static List GetMovies() { var movies = new List< Movie >(); movies.Add(new Movie (){ Name = "American Graffiti", Director = "George Lucas", DateReleased = new DateTime(1977,5,23), BoxOfficeGross = 123456}); movies.Add(new Movie () { Name = "Star Wars", Director = "George Lucas", DateReleased = new DateTime(1977, 5, 23), BoxOfficeGross = 123456 }); movies.Add(new Movie () { Name = "Empire Strikes Back", Director = "Irving Kirshner", DateReleased = new DateTime(1977, 5, 23), BoxOfficeGross = 123456 }); movies.Add(new Movie (){ Name = "Return of the Jedi", Director = "Richard Marquand", DateReleased = new DateTime(1977, 5, 23), BoxOfficeGross = 123456 }); return movies; } ``` ## Transform a list in a CSV file Looking around for a package that manages CSV file, I found [CSVHelper](https://joshclose.github.io/CsvHelper/). Also, I found another package oriented to Excel that can help you to also generate CSV called [ClosedXML](https://github.com/ClosedXML/ClosedXML) and I talked about in my post “[How to Export Data to Excel in Blazor](https://puresourcecode.com/dotnet/blazor/how-to-export-data-to-excel-in-blazor/)“. To install CSVHelper, you the following command ``` Install-Package CsvHelper ``` ## Writing CSV Files Once you’ve created your basic project, you can start by outputting a collection of data to a CSV file. The following code demonstrates the simplest mechanism for writing a collection of movie records to a CSV file. ``` public static void WriteCsvFile(List dataToWrite, string outputFile) { var config = new CsvConfiguration(CultureInfo.InvariantCulture); using (var writer = new StreamWriter(outputFile)) using (var csv = new CsvWriter(writer, config)) { csv.WriteRecords(dataToWrite); } } ``` When you examine this code, take notice of the following items: - The code creates a `CSVConfiguration` object. This object will be used to control the output of your CSV file. - The file opens a `StreamWriter` that controls where your file will be written. - The code then creates a `CSVWriter` object passing in the `configuration` object. This Writer sends your data to the stream opened by the writer using the passed-in configuration settings. - Finally, the call to `WriteRecords` routine takes an `IEnumarable` collection and writes to the CSV file. ![Movie Data Output as CSV file - Manipulating CSV Files](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/09/image-2.png?resize=640%2C179&ssl=1)Movie Data Output as CSV file ## Configuring Writer Options As stated earlier, the CSVWriter accepts a configuration object that’s used to control output options. A few of the key options will be covered next. ### No Header Column You may or may not want to include a header file in your CSV files. By default, CSVHelper adds the name of your class’ properties in a header row. You can turn off the header by setting it with the following code: ``` config.HasHeaderRecord = false; ``` ![A CSV file with no header](https://i0.wp.com/codemag.com/Article/Image/2201071/image2.PNG?w=640&ssl=1)Shows the results of this option ### Changing Delimiters One of the more common options is the delimiter used between each data element. By default, CSVHelper delimits data comma characters. The following three examples show how you can change the delimiter to the PIPE, TAB, and a “crazy” delimiter. - Changing the delimiter to PIPE: ``` config.Delimiter = "|"; ``` ![The CSV file with PIPE delimiter](https://i0.wp.com/codemag.com/Article/Image/2201071/image3.PNG?w=640&ssl=1)Shows the PIPE delimiter in action - Changing the delimiter to TAB: ``` config.Delimiter = "\t"; ``` ![The CSV file with TAB delimiter](https://i0.wp.com/codemag.com/Article/Image/2201071/image4.PNG?w=640&ssl=1)Shows the TAB delimiter in action - Creating a “Crazy” delimiter (This is just to demonstrate that your delimiter can be anything you desire): ``` config.Delimiter = "[[YES_IM_A_DELIMETER]]"; ``` **Figure 6** . ![The CSV file with the CRAZY delimiter](https://i0.wp.com/codemag.com/Article/Image/2201071/image5.PNG?w=640&ssl=1)Shows the “Crazy” delimiter doing its thing ### Quote Delimiting I’ve found in many situations that my data needs to have each data element wrapped in quotation marks. This is especially true when your data contains delimiters within their fields, e.g., commas. CSVHelper allows you to quote-delimit your data using the following options. ``` config.ShouldQuote = args => true; ``` **Figure 7** Shows the CSV with quoted content. ![The CSV file with quoted content](https://i0.wp.com/codemag.com/Article/Image/2201071/image8.PNG?w=640&ssl=1)Shows the CSV with quoted content ## Formatting Output with Map Classes Another very handy tool is the ability to control the output sent to your file. By default, CSVHelper outputs elements by reflecting on the class they come from and creating columns for each property. There are many situations where you may want to export a limited set of properties, or you wish to change the order of the output files. This is where mapping classes come in. When exporting data CSVHelper can accept a mapping object derived from the `ClassMap` class. The following code demonstrates a ClassMap that limits the data exported to two properties. ``` public class MovieOutputClassMap: ClassMap { public MovieOutputClassMap() { Map(m => m.Name); Map(m => m.DateReleased); } } ``` Once you’ve built your class map, you need to apply it to your writer. This is done using two commands. The first one creates an instance of your class map. ``` var classMap = new MovieOutputClassMap(); ``` The second registers it with the writer `Context` property: ``` csv.Context.RegisterClassMap(classMap); ``` The full writer code is shown below: ``` public static void WriteCsvFile(List dataToWrite, string outputFile) { var config = new CsvConfiguration(CultureInfo.InvariantCulture); //include header config.HasHeaderRecord = false; //change delimiter config.Delimiter = "|"; //quote delimit config.ShouldQuote = args => true; //changing the order of fields var classMap = new MovieOutputClassMap(); using (var writer = new StreamWriter(outputFile)) using (var csv = new CsvWriter(writer, config)) { csv.Context.RegisterClassMap(classMap); csv.WriteRecords(dataToWrite); } } ``` ![CSV file with only two columns exported](https://i0.wp.com/codemag.com/Article/Image/2201071/image7.PNG?w=640&ssl=1)Shows the CSV file with two columns You can also use a class map to reorder your output ``` public class MovieOutputClassMap: ClassMap { public MovieOutputClassMap() { Map(m => m.Name); Map(m => m.DateReleased); Map(m => m.Director); Map(m => m.BoxOfficeGross); } } ``` ![The CSV file columns reordered](https://i0.wp.com/codemag.com/Article/Image/2201071/image8.PNG?w=640&ssl=1)Shows the CSV file with its columns reordered Along with altering the number of columns exported and changing the ordinal position of them, you can also control the text that’s emitted into the CSV stream. Altering the output (and input) is done using a class that implements the `ITypeConverter` interface. The code below demonstrates creating a type converter that alters the output of the `DateReleased` property removing the time component. This code receives the property’s value and returns a string using the `ConvertToString` aspect of the type converter. There’s also a corollary for reading these values from strings via an implementation of the `ConvertFromString` function. ``` public class DateOutputConverter : ITypeConverter { public object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData) { throw new NotImplementedException(); } public string ConvertToString( object value, IWriterRow row, MemberMapData memberMapData) { var retval = ((DateTime) value).ToString("d"); return retval; } } ``` Once you’ve created your converter, you attach it to your column via the `mapping` class. The following code shows how to attach a converter to a property map. ``` public class MovieOutputClassMap : ClassMap { public MovieOutputClassMap() { Map(m => m.Name); Map(m => m.DateReleased).TypeConverter(new DateOutputConverter()); Map(m => m.Director); Map(m => m.BoxOfficeGross); } } ``` **Figure 10** Shows the CSV file with the date formatting altered. ![The CSV file with the date formatting altered](https://i0.wp.com/codemag.com/Article/Image/2201071/image9.PNG?w=640&ssl=1)Shows the CSV file with the date formatting altered ## Reading CSV Files Now that you have a basic understanding of writing CSV files, you can turn your sights to reading CSV files. There are two primary mechanisms for reading a file. The first is to open the file and iterate through it one record at a time. When you examine this set of code for reading files, take notice of the following items: - The code creates a `CSVConfiguration` object. This object is used to control how the reader manipulated your CSV data as it was read. - The file opens a `StreamReader`, which controls where your file will be read from. - The code then creates a `CSVReader` object passing in the `configuration` object. This reader is used to iterate through your CSV file one record at a time. - The code iterates the file using the `Read()` function, which moves down the file one record at a time. Note that the code does a `Read()` immediately, to skip the record header. - Finally, the code uses various Getter functions to read data from each column. ``` public static List ManualReadCsvFile(string inputFile) { var retval = new List(); var config = new CsvConfiguration(CultureInfo.InvariantCulture); using (var reader = new StreamReader(inputFile)) using (var csv = new CsvReader(reader, config)) { //skip the header csv.Read(); while (csv.Read()) { var movie = new Movie(); movie.Name = csv.GetField(0); movie.Director = csv.GetField(1); movie.DateReleased = csv.GetField(2); movie.BoxOfficeGross = csv.GetField(3); retval.Add(movie); } } return retval; } ``` Another and much simpler way to read a file is to use CSVHelper’s built-in mechanism for iterating through a file automatically transforming CSV records into to .NET classes. When you examine this set of code for reading files, take notice of the following items: - The code creates a `CSVConfiguration` object. This object is used to control how the reader manipulated your CSV data as it was read. - The file opens a `StreamReader`, which controls where your file will be read from. - The code then creates a `CSVReader` object passing in the `configuration` object. This reader is used to iterate through your CSV file one record at a time. - The code then reads all the records using the `GetRecords` method. This function returns an `IEnumerable` collection. - The collection is then added to the functions return value via the `AddRange()` method. ``` public static List ReadCsvFile(string inputFile) { var retval = new List(); var config = new CsvConfiguration(CultureInfo.InvariantCulture); using (var reader =new StreamReader(inputFile)) using (var csv = new CsvReader(reader, config)) { retval.AddRange(csv.GetRecords()); } return retval; } ``` As you can see, this style of code is much simpler to deal with. You can also use class maps to change the order of how CSV elements are read from your CSV file and are applied to the returned object’s properties. The following class map reads content from the CSV created earlier in this article. Notice the column order. ``` public class MovieInputClassMap : ClassMap { public MovieInputClassMap() { Map(m => m.Name); Map(m => m.DateReleased); Map(m => m.Director); Map(m => m.BoxOfficeGross); } } ``` The code used to attach a class map is exactly like the writer. You simply create an instance of the class map and apply it to the CSVReader’s `Context` property: ``` public static List ReadCsvFile(string inputFile) { var retval = new List(); var config = new CsvConfiguration(CultureInfo.InvariantCulture); var classMap = new MovieInputClassMap(); using (var reader = new StreamReader(inputFile)) using (var csv = new CsvReader(reader, config)) { csv.Context.RegisterClassMap(classMap); retval.AddRange(csv.GetRecords()); } return retval; } ``` **Categories:** .NET, .NET Core, .NET General, .NET5, .NET6, C# **Tags:** csv, export **Hashtags:** csv, export, import --- ### [Evaluate Postfix expression for interviews](https://puresourcecode.com/dotnet/net-core/evaluate-postfix-expression-for-interviews/) **Published:** September 21, 2022 **Author:** Enrico **Excerpt:** I will show how to evaluate Postfix expression for your interviews. What is a Postfix? How to write a simple code to evaluate an expression? **Content:** In this new post, I will show how to evaluate Postfix expression for your interviews as a [C#](https://puresourcecode.com/category/dotnet/csharp/) developer. When you have interviews, you never know what they will ask you. Sometimes, the easiest question could be the most complicated thing. I don’t know you, but I found this snap test interview quite boring. For example, when in the real work you have to evaluate a Postfix expression? The source code of this test is on [GitHub](https://github.com/erossini/ReversePostfix). ## Reverse Polish notation (RPN) or Postfix notation Reverse Polish notation ([RPN](https://en.wikipedia.org/wiki/Reverse_Polish_notation)), also known as reverse Łukasiewicz notation, Polish postfix notation or simply postfix notation, is a mathematical notation in which operators follow their operands, in contrast to Polish notation (PN), in which operators precede their operands. It does not need any parentheses as long as each operator has a fixed number of operands. The description “Polish” refers to the nationality of logician Jan Łukasiewicz, who invented Polish notation in 1924. In reverse Polish notation, the operators follow their operands; for instance, to add 3 and 4 together, one would write `3 4 +` rather than `3 + 4`. If there are multiple operations, operators are given immediately after their final operands (often an operator takes two operands, in which case the operator is written after the second operand); so, the expression written `3 − 4 + 5` in conventional notation would be written `3 4 − 5 +` in reverse Polish notation: 4 is first subtracted from 3, then 5 is added to it. An advantage of reverse Polish notation is that it removes the need for parentheses that are required by infix notation. While `3 − 4 × 5` can also be written `3 − (4 × 5)`, that means something quite different from `(3 − 4) × 5`. In reverse Polish notation, the former could be written `3 4 5 × −`, which unambiguously means `3 (4 5 ×)` − which reduces to `3 20` − (which can further be reduced to `-17`); the latter could be written `3 4 − 5 ×` (or `5 3 4 − ×`, if keeping similar formatting), which unambiguously means `(3 4 −) 5 ×`. ## The initial code After 2 hours of discussion about interesting topics, they showed me the following code: ``` using System; using System.Collections.Generic; namespace ReversePostfix { public class Test { public static void Main() { RunTestCase(10, new[] { "5", "2", "*" }); // 5 * 2 = 10 RunTestCase(13, new[] { "10", "2", "*", "6", "+", "2", "/" }); // ((10 * 2) + 6) / 2 = 13 RunTestCase(95, new[] { "100", "10", "-", "30", "6", "/", "+" }); // (100 - 10) + (30 / 6) = 95 RunTestCase(6, new[] { "10", "7", "-", "!" }); // (10 - 7)! = 6 } public static double Calculate(IEnumerable input) { } // --------------------------------------------------------------------------- public static double CalculateFactorial(double input) { if (input > 100) { throw new InvalidOperationException("This will take too long for the interview."); } var num = input; for (var i = 1; i < input; ++i) { num *= input - i; } return num; } public static void RunTestCase(double expectedResult, IEnumerable input) { var result = Calculate(input); if (Math.Abs(result - expectedResult) < 0.001) { Console.WriteLine("Pass"); } else { Console.WriteLine("Fail: Expected = {0}, Actual = {1}. Input = '{2}'", expectedResult, result, string.Join(",", input)); } } } } ``` So, what is this code doing? Obviously, `Main` is calling the function `RunTestCase` that checks if the function `Calculate` returns the expected value. The function `CalculateFactorial` is totally useless. Now, if you don’t know the Postfix notation, you can’t solve the problem in the academic way. So, the first question passes in my mind is: do I really want to work with them? ## Using Stack A Stack represents a last-in, first-out collection of objects. It is used when you need last-in, first-out access to items. It is both a generic and non-generic type of collection. The generic stack is defined in `System.Collections.Generic` namespace whereas non-generic stack is defined under `System.Collections` namespace, here we will discuss non-generic type stack. A stack is used to create a dynamic collection that grows, according to the need of your program. In a stack, you can store elements of the same type or different types. ![Stack representation - Evaluate Postfix expression for interviews](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/09/image.png?resize=221%2C465&ssl=1)Stack representation The below diagram illustrates the Stack class hierarchy: ![Stack class hierarchy - Evaluate Postfix expression for interviews](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/09/image-1.png?resize=231%2C501&ssl=1)Stack class hierarchy ### Functions of Stack in C# Whenever we need access to the elements of the stack in last in and first out order, we the collection of objects called `Stack`. The process of adding an element to the Stack is called pushing the elements to the stack and the process of removing an element from the stack is called popping an element from the Stack. Stack is a dynamic collection of elements because the size of the stack increases with the addition of elements to the stack. The number of elements that a stack can hold is called the capacity of the stack. As the size of the stack increases with the addition of elements to the stack, the capacity of the stack also increases through reallocation. There can be duplicate elements allowed in the stack. `Null` is accepted by the stack as a valid value for type, references. There are several constructors in Stack in C#. They are: - **Stack():** A new instance of the stack class is initialized which is empty whose initial capacity is the default. - **Stack(ICollection):** A new instance of the stack class is initialized which consists of elements that are taken from a collection specified as a parameter and the initial capacity is the same as the number of elements taken from the collection specified as a parameter. - **Stack(Int32):** A new instance of the stack class is initialized which is empty whose initial capacity is either the initial capacity specified as the parameter or the initial capacity which is default. ### Methods in C# Stack There are several methods in Stack in C#. They are: - **Clear():** The objects of the stack are removed using the `Clear()` method. - **Push(Object):** An object specified as the parameter is inserted at the top of the stack using the `Push(Object)` method. - **Contains(Object):** The `Contains(Object)` method is used to determine if an element is present in the stack. - **Peek():** The object specified at the top of the stack is returned but is not removed using the `Peek()` method. - **Pop():** The object specified at the top of the stack is returned and is removed using the `Pop()` method. ### How to create a Stack So, this is a simple definition of a `Stack` and remember to add the `using System.Collections` ``` using System.Collections; Stack stack_name = new Stack(); ``` ## How to use Stack Now, see the following code: ``` // C# program to illustrate how to // create a stack using System; using System.Collections; class StackExample { // Main Method static public void Main() { // Create a stack // Using Stack class Stack my_stack = new Stack(); // Adding elements in the Stack // Using Push method my_stack.Push("PureSourceCode"); my_stack.Push("psc"); my_stack.Push('ER'); my_stack.Push(null); my_stack.Push(1234); my_stack.Push(490.98); // Accessing the elements // of my_stack Stack // Using foreach loop foreach(var elem in my_stack) { Console.WriteLine(elem); } } } ``` The output is: ``` 490.98 1234 ER psc PureSourceCode ``` ### How to remove a Stack So, in Stack, you are allowed to remove elements from the stack. The Stack class provides two different methods to remove elements and the methods are: - **Clear:** This method is used to remove all the objects from the stack. - **Pop:** This method removes the beginning element of the stack. ``` // C# program to illustrate how to // remove elements from the stack using System; using System.Collections; class StackExample { // Main Method static public void Main() { // Create a stack // Using Stack class Stack my_stack = new Stack(); // Adding elements in the Stack // Using Push method my_stack.Push("PureSourceCode"); my_stack.Push("psc"); my_stack.Push("123"); my_stack.Push("puresourcecode"); Console.WriteLine("Total elements present in"+ " my_stack: {0}", my_stack.Count); my_stack.Pop(); // After Pop method Console.WriteLine("Total elements present in "+ "my_stack: {0}", my_stack.Count); // Remove all the elements // from the stack my_stack.Clear(); // After Pop method Console.WriteLine("Total elements present in "+ "my_stack: {0}", my_stack.Count); } } ``` The output is: ``` Total elements present in my_stack: 4 Total elements present in my_stack: 3 Total elements present in my_stack: 0 ``` ### How to get the topmost element of the Stack? In Stack, you can easily find the topmost element of the stack by using the following methods provided by the Stack class: - **Pop:** This method returns the object at the beginning of the stack with modification means this method removes the topmost element of the stack. - **Peek:** This method returns the object at the beginning of the stack without removing it. ``` // C# program to illustrate how to // get topmost elements of the stack using System; using System.Collections; class StackExample { // Main Method static public void Main() { // Create a stack // Using Stack class Stack my_stack = new Stack(); // Adding elements in the Stack // Using Push method my_stack.Push("PureSourceCode"); my_stack.Push("psc"); my_stack.Push("123"); my_stack.Push("puresourcecode"); Console.WriteLine("Total elements present in"+ " my_stack: {0}",my_stack.Count); // Obtain the topmost element // of my_stack Using Pop method Console.WriteLine("Topmost element of my_stack" + " is: {0}",my_stack.Pop()); Console.WriteLine("Total elements present in"+ " my_stack: {0}", my_stack.Count); // Obtain the topmost element // of my_stack Using Peek method Console.WriteLine("Topmost element of my_stack "+ "is: {0}",my_stack.Peek()); Console.WriteLine("Total elements present "+ "in my_stack: {0}",my_stack.Count); } } ``` The output is: ``` Total elements present in my_stack: 4 Topmost element of my_stack is: puresourcecode Total elements present in my_stack: 3 Topmost element of my_stack is: 123 Total elements present in my_stack: 3 ``` ### How to check the availability of elements in the stack? In a stack, you can check whether the given element is present or not using [Contains()](https://www.geeksforgeeks.org/stack-contains-method-in-c-sharp/) method. Or in other words, if you want to search an element in the given stack use Contains() method. This method returns true if the element present in the stack. Otherwise, return false. Note: The `Contains()` method takes O(n) time to check if the element exists in the stack. This should be taken into consideration while using this method. ``` using System; using System.Collections; class StackExample { // Main Method static public void Main() { // Create a stack // Using Stack class Stack my_stack = new Stack(); // Adding elements in the Stack // Using Push method my_stack.Push("PureSourceCode"); my_stack.Push("psc"); my_stack.Push("123"); my_stack.Push("puresourcecode"); // Checking if the element is // present in the Stack or not if (my_stack.Contains("PureSourceCode") == true) { Console.WriteLine("Element is found...!!"); } else { Console.WriteLine("Element is not found...!!"); } } } ``` The output is: ``` Element is found...!! ``` ### Generic Stack Vs Non-Generic Stack Generic StackNon-Generic StackGeneric stack is defined under System.Collections.Generic namespace.Non-Generic stack is defined under System.Collections namespace.Generic stack can only store same type of elements.Non-Generic stack can store same type or different types of elements.There is a need to define the type of the elements in the stack.There is no need to define the type of the elements in the stack.It is type-safe.It is not type-safe.Postfix ## Evaluate Postfix expression So, now we have some context and clarity about the `Stack`, we can start to evaluate Postfix expression for our interviews. First, have a look at the test cases ``` RunTestCase(10, new[] { "5", "2", "*" }); // 5 * 2 = 10 RunTestCase(13, new[] { "10", "2", "*", "6", "+", "2", "/" }); // ((10 * 2) + 6) / 2 = 13 RunTestCase(95, new[] { "100", "10", "-", "30", "6", "/", "+" }); // (100 - 10) + (30 / 6) = 95 RunTestCase(6, new[] { "10", "7", "-", "!" }); // (10 - 7)! = 6 ``` Look at the first line. I read from the array the first element and I save it in a variable. Then, I read the second element. I have to check if it is an operator and, if it is not, save it in another variable. Then, I read the last value, check if it is an operator and then perform the calculation. Then, you have the last test. In this case, there is an `!` operator. If you see the result of the test, the value is 6. So, I think in this case the exclamation point multiplies the value with 2. Now, I have to repeat the process for each element of the array. **Don’t be scared about the parenthesis!** This is a simple test how to evaluate Postfix expression for interviews. Do you think the interviewers what to spend a lot of time with you? Don’t do like me: think the simplest solution you can. For example, I was thinking how to use `Linq` or other complicated structure. Keep it simple. They are not too smart 😀 This test is an academic code designed to see if you know the basic commands. So, the best code I can write for that is the following: ``` using System; using System.Collections.Generic; using System.Linq; namespace ReversePostfix { public class Test { public static void Main() { RunTestCase(10, new[] { "5", "2", "*" }); // 5 * 2 = 10 RunTestCase(13, new[] { "10", "2", "*", "6", "+", "2", "/" }); // ((10 * 2) + 6) / 2 = 13 RunTestCase(95, new[] { "100", "10", "-", "30", "6", "/", "+" }); // (100 - 10) + (30 / 6) = 95 RunTestCase(6, new[] { "10", "7", "-", "!" }); // (10 - 7)! = 6 } public static double Calculate(IEnumerable input) { Stack stack = new Stack(); foreach (string element in input) { if (!"+-*/!".Contains(element)) { stack.Push(Convert.ToDouble(element)); continue; } double second = stack.Count() > 0 ? stack.Pop() : 0; double first = stack.Count() > 0 ? stack.Pop() : 0; double ans = 0; switch (element) { case "+": ans = first + second; break; case "-": ans = first - second; break; case "/": ans = first / second; break; case "*": ans = first * second; break; case "!": ans = second * 2; break; } stack.Push(ans); } return stack.Pop(); } public static void RunTestCase(double expectedResult, IEnumerable input) { var result = Calculate(input); if (Math.Abs(result - expectedResult) < 0.001) { Console.WriteLine("Pass"); } else { Console.WriteLine("Fail: Expected = {0}, Actual = {1}. Input = '{2}'", expectedResult, result, string.Join(",", input)); } } } } ``` ## Wrap up In conclusion, this is an implementation of how evaluate Postfix expression for interviews. I hope this code can help you. Please use the comment below or the [forum](https://puresourcecode.com/forum/) to give me your feedback or suggest more tests. **Categories:** .NET Core **Tags:** c#, interviews **Hashtags:** postfix --- ### [Derived classes with System.Text.Json](https://puresourcecode.com/dotnet/net-core/derived-classes-with-system-text-json/) **Published:** August 23, 2022 **Author:** Enrico **Excerpt:** In this post I show you how to use System.Text.Json and how to implement a converter for polymorphic classes. **Content:** In this post I show you how to convert derived classes with `System.Text.Json` and how to implement a converter for polymorphic classes. ## Issue I’m tried to object a JSON with `System.Text.Json` from my class. I have a base class called `Element` that is defined like this ``` public interface IElement { public string? Type { get; set; } public string? Name { get; set; } } ``` Then, I have few classes that inherit from it, for example ``` public class Textbox : IElement { [JsonPropertyName("type")] public virtual string? Type { get; set; } [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("text")] public string? Text { get; set; } } public class Radiobutton : IElement { [JsonPropertyName("type")] public virtual string? Type { get; set; } [JsonPropertyName("name")] public string? Name { get; set; } [JsonPropertyName("choises")] public List Choises = new List(); } ``` Now, I want to have a class that defines the form with all the elements ``` public class Form { [JsonPropertyName("elements")] public List Elements { get; set; } = new List(); } ``` After that, I define the form ``` Form form = new Form() { Elements = new List() { new Textbox() { Name = "txt1", Type = "Textbox", Text = "One" }, new Radiobutton() { Name = "radio1", Type = "Radiobutton", Choices = new List() { "One", "Two", "Three" }} } }; ``` If I create the JSON from this object, it has only the common fields ``` { "elements": [ { "type": "Textbox", "name": "txt1", }, { "type": "Radiobutton", "name": "radio1", } ] } ``` The fields `Text` for the Textbox or `Choices` for the Radiobutton are ignored. I read the [Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-polymorphism): I tried the code ``` jsonString = JsonSerializer.Serialize(weatherForecast, options); ``` but I obtained the same result. How can I create the JSON with all the details of the `Form` object regardless of the type of `Element`? Viceversa, when I have the JSON, how can I deserialize it in the `Form` class? ## Solution I like to share with you an issue I found using `System.Text.Json`. I followed the approach `TypeDiscriminatorConverter` that [Demetrius Axenowski](https://stackoverflow.com/users/4040476/demetrius-axenowski). It works very well. My problems started when I added some annotations for the JSON. For example: ``` [JsonPropertyName("name")] ``` I have lost all day to understand why the code didn’t work. I created some dummy code to understand where the problem was. All the source code is now on [GitHub](https://github.com/erossini/JsonPolymorphicConverter). So, the problem was in the `JsonPropertyName` for the property I check in the converter. For example, this is a class ``` public class Radiobutton : ElementBase { [JsonPropertyName("type")] public string Type => "Radiobutton"; public ElementType ElementType = ElementType.Radiobutton; public List? Choices { get; set; } } ``` As you can see, I set the `JsonPropertyName` because I like to see `type` in lower case. Now, if I convert the class with this converter: ``` public class ElementTypeConverter : JsonConverter where T : IElementType { private readonly IEnumerable _types; public ElementTypeConverter() { var type = typeof(T); _types = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract) .ToList(); } public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException(); } using (var jsonDocument = JsonDocument.ParseValue(ref reader)) { if (!jsonDocument.RootElement.TryGetProperty( nameof(IElementType.Type), out var typeProperty)) { throw new JsonException(); } var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString()); if (type == null) { throw new JsonException(); } var jsonObject = jsonDocument.RootElement.GetRawText(); var result = (T)JsonSerializer.Deserialize(jsonObject, type, options); return result; } } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, (object)value, options); } } ``` I get the following error: > Test method SurveyExampleNetStardard21.Tests.UnitTest1.TestConversionJson\_SystemTextJson\_3Textbox\_1radiobutton threw exception: > > System.Text.Json.JsonException: The JSON value could not be converted to System.Collections.Generic.List`1\[SurveyExampleNetStardard21.Interfaces.IElement\]. Path: $.Elements\[3\] | LineNumber: 42 | BytePositionInLine: 5. I removed the `JsonPropertyName` and it works fine. I tried to set ``` [JsonPropertyName("Type")] ``` (basically, the same as the variable) and it works fine. So, don’t change the name. The converter is working both ways (object to Json and Json to object). This is the test code: ``` var jsonSerializerOptions = new JsonSerializerOptions() { Converters = { new ElementTypeConverter() }, WriteIndented = true }; var json = JsonSerializer.Serialize(form, jsonSerializerOptions); var back = JsonSerializer.Deserialize(json, jsonSerializerOptions); var json2 = JsonSerializer.Serialize(back, jsonSerializerOptions); ``` Another annotation is related to `Newtonsoft.Json`: I converted the object to Json, and it was good without any particular configuration. When I tried to convert the result Json in the object, I got issues in the conversion. **Categories:** .NET Core, .NET General, .NET5, .NET6, C# **Tags:** converter, derived-classes, json, netstandard **Hashtags:** converters, json, netstandard --- ### [Add SQLite to the MAUI application](https://puresourcecode.com/dotnet/maui/add-sqlite-to-the-maui-application/) **Published:** August 12, 2022 **Author:** Enrico **Excerpt:** I show you how to add SQLite to the MAUI application in order to have a small database in your mobile applications **Content:** ![sql sqlite wallpaper](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/08/sql-sqlite-wallpaper.jpg?resize=240%2C172&ssl=1)In this new post, I show you how to add SQLite to the [MAUI](https://puresourcecode.com/?s=maui) application in order to have a small database in your mobile applications. ## Add NuGet packages First of all, we need to install [NuGet](https://www.nuget.org/packages/sqlite-net-pcl) packages. Here the packages we have to install for using SQLite in the MAUI applications: ``` ``` ## File Access Helper Now, we have to create a helper to locate the database. This is the class ``` public class FileAccessHelper { public static string GetLocalFilePath(string filename) { return System.IO.Path.Combine(FileSystem.AppDataDirectory, filename); } } ``` After that, in the `MauiProgram.cs`, add the following (highlighted) line ``` public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); string dbPath = FileAccessHelper.GetLocalFilePath("my.db"); builder.Services.AddSingleton( s => ActivatorUtilities.CreateInstance(s, dbPath)); return builder.Build(); } } ``` ## Create a repository Then create repository class as in the following code: ``` private readonly SQLiteAsyncConnection _database; public string StatusMessage { get; set; } public Repository() { _dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "my.db"); } private async Task Init() { if (_db != null) return; _db = new SQLiteAsyncConnection(_dbPath); await _db.CreateTableAsync(); } public List List() { try { await Init(); return await _db.Table().ToListAsync(); } catch (Exception ex) { StatusMessage = $"Failed to retrieve data. {ex.Message}"; } return new List(); } public int Create(MyEntity entity) { int result = 0; try { await Init(); if (string.IsNullOrEmpty(entity.Address)) throw new Exception("Valid address required"); result = await _db.InsertAsync(entity); StatusMessage = string.Format("{0} record(s) added [Name: {1})", result, entity.Address); } catch (Exception ex) { StatusMessage = string.Format("Failed to add {0}. Error: {1}", entity.Address, ex.Message); } } public int Update(MyEntity entity) { return _database.Update(entity); } public int Delete(MyEntity entity) { return _database.Delete(entity); } ``` ## How to use it So far, I am following the Microsoft recommendation. The next step is to change the `App.xaml.cs` to inject the repository ``` public partial class App : Application { public static Repository Repo { get; private set; } public App(Repository repo) { InitializeComponent(); MainPage = new AppShell(); Repo = repo; } } ``` When in a page you have to use the repo, you should refer to the `Repo` in the `App` like that ``` await App.Repo.Create(new MyEntity() { ... }); StatusMessage.Text = App.HomeRepo.StatusMessage; ``` Another way is to initialize the repository in the page or viewmodel you want like in the following example: ``` private readonly Repository repository; public MainPage() { repository = new Repository(); InitializeComponent(); } protected override void OnAppearing() { base.OnAppearing(); GetEntities(); } private void GetEntities() { collectionView.ItemsSource = repository.List(); } ``` ## Important part for iOS For iOS/MacCatalyst we need to set the SQLite provider. We can do it in `AppDelegate`: ``` protected override MauiApp CreateMauiApp() { raw.SetProvider(new SQLite3Provider_sqlite3()); return MauiProgram.CreateMauiApp(); } ``` **Categories:** MAUI **Tags:** maui, sqlite --- ### [Drag and drop with Blazor](https://puresourcecode.com/dotnet/blazor/drag-and-drop-with-blazor/) **Published:** August 12, 2022 **Author:** Enrico **Excerpt:** I show how to implement drag and drop with Blazor because drag and drop has become a popular interface solution in modern applications. **Content:** In this new post, I show how to implement drag and drop with [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). It’s common to find drag and drop interfaces in productivity tools, great examples of this is [Azure DevOps](https://puresourcecode.com/tools/azure-devops/azure-devops-processes/). As well as being an intuitive interface for the user, it can definitely add a bit of “eye-candy” to an application. As result of this post, I want to create a simple [Kanban board](https://agile.puresourcecode.com/#/./kanban/kanban-board) like in the following screenshot. ![Drag and drop with Blazor - Kanban board](https://i0.wp.com/github.com/erossini/BlazorDragAndDrop/raw/main/Screenshots/KanbanBoard-Example.gif?w=640&ssl=1)Drag and drop with Blazor – Kanban board Before that, Before that, I look at a simple example with a bullet list. ![Drag and drop with Blazor - Reorder list](https://i0.wp.com/github.com/erossini/BlazorDragAndDrop/raw/main/Screenshots/ReoderList-Example.gif?w=640&ssl=1)Drag and drop with Blazor – Reorder list And then a little bit complex example to play around with. ![](https://i0.wp.com/github.com/erossini/BlazorDragAndDrop/raw/main/Screenshots/ReoderList2-Example.gif?w=640&ssl=1) The source code of both projects is on [GitHub](https://github.com/erossini/BlazorDragAndDrop). ## Drag and drop API The [drag and drop API is part of the HTML5 spec](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API) and has been around for a long time now. The API defines a set of events and interfaces. We can use them to build a drag and drop interface. ### Events - `drag` Fires when a *dragged item* (element or text selection) is dragged. - `dragend` Fires when a drag operation ends, such as releasing a mouse button or hitting the Esc key. - `dragenter` Fires when a dragged item enters a valid drop target. - `dragexit` Fires when an element is no longer the drag operation’s immediate selection target. - `dragleave` Fires when a dragged item leaves a valid drop target. - `dragover` Fires when a dragged item is being dragged over a valid drop target, every few hundred milliseconds. - `dragstart` Fires when the user starts dragging an item. - `drop` Fires when an item is dropped on a valid drop target. Certain events will only fire once during a drag-and-drop interaction such as `dragstart` and `dragend`. However, others will fire repeatedly such as `drag` and `dragover`. ### Interfaces There are a few interfaces for drag and drop interactions but the key ones are the `DragEvent` interface and the `DataTransfer` interface. The `DragEvent` interface is a DOM event which represents a drag and drop interaction. It contains a single property, `dataTransfer`, which is a `DataTransfer` object. The `DataTransfer` interface has several properties and methods available. It contains information about the data being transferred by the interaction as well as methods to add or remove data from it. #### **Properties** - `dropEffect` Gets the type of drag-and-drop operation currently selected or sets the operation to a new type. The value must be `none`, `copy`, `link` or `move`. - `effectAllowed` Provides all of the types of operations that are possible. Must be one of `none`, `copy`, `copyLink`, `copyMove`, `link`, `linkMove`, `move`, `all` or `uninitialized`. - `files` Contains a list of all the local files available on the data transfer. If the drag operation doesn’t involve dragging files, this property is an empty list. - `items` Gives a [`DataTransferItemList`](https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItemList) object which is a list of all of the drag data. - `types` An array of [`strings`](https://developer.mozilla.org/en-US/docs/Web/API/DOMString) giving the formats that were set in the `dragstart` event. #### **Methods** - `DataTransfer.clearData()` Remove the data associated with a given type. The type argument is optional. If the type is empty or not specified, the data associated with all types is removed. If data for the specified type does not exist, or the data transfer contains no data, this method will have no effect. - `DataTransfer.getData()` Retrieves the data for a given type, or an empty string if data for that type does not exist or the data transfer contains no data. - `DataTransfer.setData()` Set the data for a given type. If data for the type does not exist, it is added at the end, such that the last item in the types list will be the new format. If data for the type already exists, the existing data is replaced in the same position. - `DataTransfer.setDragImage()` Set the image to be used for dragging if a custom one is desired. ## Reorder list project So, based on the drag and drop API from HTML5, I am going to create a first basic example. Open the `Index.razor` page and add the following HTML code ``` @page "/" @foreach (var item in Models.OrderBy(x => x.Order)) { @item.Name } ``` How you can see in the code, for each API event I added a specific function for Blazor. The tag `ul` is the generic container of the drag and drop actions: here I defined to call `ondragover` and `ondragstart`. I connect the other API events at the `li` level because those are the elements that are changing their status. ### The model Now, in the `code` section, I defined a simple model for each element I want to display. ``` public List Models { get; set; } = new(); public class Model { public int Order { get; set; } public string Name { get; set; } = ""; public bool IsDragOver{ get; set; } } // the model that is being dragged private Model? draggingModel; ``` So, in the `OnInitialized` I’m going to create at runtime 10 random elements ``` protected override void OnInitialized() { // fill names with "random" string for (var i = 0; i < 10; i++) { Model m = new() { Order = i, Name = $"Item {i}" }; Models.Add(m); } base.OnInitialized(); } ``` ### Handle the drop The last part is to manage the drop in Blazor and for this reason there is a function called `HandleDrop` that it is called from `li ondrop`. This is the C# function ``` private void HandleDrop(Model landingModel) { // landing model -> where the drop happened if (draggingModel is null) return; // keep the original order for later int originalOrderLanding = landingModel.Order; // increase model under landing one by 1 Models.Where(x => x.Order >= landingModel.Order).ToList().ForEach(x => x.Order++); // replace landing model draggingModel.Order = originalOrderLanding; int ii = 0; foreach (var model in Models.OrderBy(x=>x.Order).ToList()) { // keep the numbers from 0 to size-1 model.Order = ii++; // remove drag over. model.IsDragOver = false; } } ``` This function receives as a parameter, the item the user moved in the new position. When the drag starts, the variable `draggingModel` has the full `item` from the `Model`. If the new item position is a valid one, I keep the original order in `originalOrderLanding` and I increse the `Order` value for all the elements from the position in advance. Then, I order the item list again and update the order. ## Reorder list with complex elements Based on the example we have just seen, we can change it with a bit more complex element to drag. Also, I want to display a red line to show the user the exact position of the drop of the element. ``` @foreach (var item in Models.OrderBy(x => x.Order)) { @item.Name Child elem. to demonstrate the issue @item.Name @if (draggingModel is not null) { } } ``` For that, in the `div` I added an instant `if`: if the user is dragging the item, a red line appears under the item the mouse is passing over. ![Drag and drop with Blazor - Reorder list](https://i0.wp.com/github.com/erossini/BlazorDragAndDrop/raw/main/Screenshots/ReoderList-Example.gif?w=640&ssl=1)Drag and drop with Blazor – Reorder list ## Simple Kanban board Now, we talked about drag and drop with simple elements, we can head to create a more complex example, like a simple Kanban board. ### Build the project As you have seen from the gif at the start of this post, the prototype is a highly original todo list. I set myself some goals I wanted to achieve from the exercise, they were: - Be able to track an item being dragged - Control where items could be dropped - Give a visual indicator to the user where items could be dropped or not dropped - Update an item on drop - Feedback when an item has been updated #### Overview My solution ended up with three components, `JobsContainer`, `JobList` and `Job` which are used to manipulate a list of `JobModel`s. ``` public class JobModel { public int Id { get; set; } public JobStatuses Status { get; set; } public string Description { get; set; } public DateTime LastUpdated { get; set; } } ``` Then, we define the enum for the statues of the jobs. ``` public enum JobStatuses { Todo, Started, Completed } ``` The `JobsContainer` is responsible for overall list of jobs, keeping track of the job being dragged and raising an event whenever a job is updated. So, the `JobsList` component represents a single job status, it creates a *drop-zone* where jobs can be dropped and renders any jobs which have its status. At the end, the `Job` component renders a `JobModel` instance. If the instance is dragged, then it lets the `JobsContainer` know so it can be tracked. #### JobsContainer Component ``` @ChildContent @code { [Parameter] public List Jobs { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public EventCallback OnStatusUpdated { get; set; } public JobModel Payload { get; set; } public async Task UpdateJobAsync(JobStatuses newStatus) { var task = Jobs.SingleOrDefault(x => x.Id == Payload.Id); if (task != null) { task.Status = newStatus; task.LastUpdated = DateTime.Now; await OnStatusUpdated.InvokeAsync(Payload); } } } ``` ##### Code explained The job of `JobsContainer` job is to coordinate updates to jobs as they are moved about the various statuses. It takes a list of `JobModel` as a parameter as well as exposing an event which consuming components can handle to know when a job gets updated. It passes itself as a `CascadingValue` to the various `JobsList` components, which are child components. This allows them access to the list of jobs as well as the `UpdateJobAsync` method, which is called when a job is dropped onto a new status. #### JobsList Component ``` @ListStatus (@Jobs.Count()) @foreach (var job in Jobs) { } @code { [CascadingParameter] JobsContainer Container { get; set; } [Parameter] public JobStatuses ListStatus { get; set; } [Parameter] public JobStatuses[] AllowedStatuses { get; set; } List Jobs = new List(); string dropClass = ""; protected override void OnParametersSet() { Jobs.Clear(); Jobs.AddRange(Container.Jobs.Where(x => x.Status == ListStatus)); } private void HandleDragEnter() { if (ListStatus == Container.Payload.Status) return; if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status)) { dropClass = "no-drop"; } else { dropClass = "can-drop"; } } private void HandleDragLeave() { dropClass = ""; } private async Task HandleDrop() { dropClass = ""; if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status)) return; await Container.UpdateJobAsync(ListStatus); } } ``` ##### Code explained There is quite a bit of code so let’s break it down. ``` [Parameter] JobStatuses ListStatus { get; set; } [Parameter] JobStatuses[] AllowedStatuses { get; set; } ``` The component takes a `ListStatus` and array of `AllowedStatuses`. The `AllowedStatuses` are used by the `HandleDrop` method to decide if a job can be dropped or not. Then, the `ListStatus` is the job status that the component instance is responsible for. It’s used to fetch the jobs from the `JobsContainer` component which match that status so the component can render them in its list. This is performed using the `OnParametersSet` lifecycle method, making sure to clear out the list each time to avoid duplicates. ``` protected override void OnParametersSet() { Jobs.Clear(); Jobs.AddRange(Container.Jobs.Where(x => x.Status == ListStatus)); } ``` ##### Ordering the list I’m using an unordered list to display the jobs. The list is also a *drop-zone* for jobs, meaning you can drop other elements onto it. This is achieved by defining the `ondragover` event but note there’s no `@` symbol in-front of it. ``` @foreach (var job in Jobs) { } ``` ##### Prevent default The event is just a normal JavaScript event, not a Blazor version, calling `preventDefault`. The reason for this is that by default you can’t drop elements onto each other. By calling `preventDefault` it stops this default behaviour from occurring. I’ve also defined the `ondragstart` JavaScript event as well, this is there to satisfy FireFoxs requirements to enable drag and drop and doesn’t do anything else. ##### Handle the drag The rest of the events are all Blazor versions. `OnDragEnter` and `OnDragLeave` are both used to set the CSS of for the *drop-zone.* ``` private void HandleDragEnter() { if (ListStatus == Container.Payload.Status) return; if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status)) { dropClass = "no-drop"; } else { dropClass = "can-drop"; } } private void HandleDragLeave() { dropClass = ""; } ``` `HandleDragEnter` manages the border of the *drop-zone* to give the user visual feedback. If the job being dragged has the same status as the *drop-zone* it’s over then nothing happens. If a job is dragged over the *drop-zone,* and it’s a valid target, then a green border is added via the `can-drop` CSS class. If it’s not a valid target then a red border is added via the `no-drop` CSS class. The `HandleDragLeave` method just resets the class once the job has been dragged away. ``` private async Task HandleDrop() { dropClass = ""; if (AllowedStatuses != null && !AllowedStatuses.Contains(Container.Payload.Status)) return; await Container.UpdateJobAsync(ListStatus); } ``` Finally, `HandleDrop` is responsible for making sure a job is allowed to be dropped, and if so, updating its status via the `JobsContainer`. #### Job Component ``` @JobModel.Description Last Updated @JobModel.LastUpdated.ToString("HH:mm.ss tt") @code { [CascadingParameter] JobsContainer Container { get; set; } [Parameter] public JobModel JobModel { get; set; } private void HandleDragStart(JobModel selectedJob) { Container.Payload = selectedJob; } } ``` ##### Code explained It’s responsible for displaying a `JobModel` and for making it draggable. Elements are made draggable by adding the `draggable="true"` attribute. The component is also responsible for handling the `ondragstart` event. When `ondragstart` fires the component assigns the job to the `JobsContainer`s `Payload` property. This keeps track of the job being dragged which is used when handling drop events, as we saw in the `JobsList` component. #### Usage Now we’ve gone through each component let’s see what it looks like all together. ``` @code { List Jobs = new List(); protected override void OnInitialized() { Jobs.Add(new JobModel { Id = 1, Description = "Install certicate for the website", Status = JobStatuses.Todo, LastUpdated = DateTime.Now }); Jobs.Add(new JobModel { Id = 2, Description = "Fix bug in the drag and drop project", Status = JobStatuses.Todo, LastUpdated = DateTime.Now }); Jobs.Add(new JobModel { Id = 3, Description = "Update NuGet packages", Status = JobStatuses.Todo, LastUpdated = DateTime.Now }); Jobs.Add(new JobModel { Id = 4, Description = "Generate graphs", Status = JobStatuses.Todo, LastUpdated = DateTime.Now }); Jobs.Add(new JobModel { Id = 5, Description = "Finish blog post", Status = JobStatuses.Started, LastUpdated = DateTime.Now }); } void HandleStatusUpdated(JobModel updatedJob) { Console.WriteLine(updatedJob.Description); } } ``` **Categories:** Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly, drag-and-drop --- ### [Use biometric authentication in MAUI](https://puresourcecode.com/dotnet/maui/use-biometric-authentication-in-maui/) **Published:** August 11, 2022 **Author:** Enrico **Excerpt:** In this post I show how to use biometric authentication in MAUI in your applications for iOS and Android and in machines with Windows Hello **Content:** In this new post, I am going to explain how to use biometric authentication with [MAUI](https://puresourcecode.com/dotnet/net-core/start-with-maui/) in your mobile or desktop applications. Biometric authentication has become an increasingly integral part of mobile apps to ensure that the user is the rightful owner of the device that they’re using. Here’s how you can authenticate via Face ID (iOS) or fingerprint (Android / iOS) in your .NET MAUI app. Also, it is integrate in [Windows 11](https://puresourcecode.com/?s=Windows%2011): so, if you have Windows Hello enabled on your machine, the biometric authentication is available on desktops and laptops. ## Add the package First, create a new MAUI project in [Visual Studio 2022 Preview](https://puresourcecode.com/dotnet/net-core/install-maui-with-visual-studio-2022-preview/). Then, install the [Plugin.Fingerprint](https://github.com/smstuebe/xamarin-fingerprint) NuGet package. You’ll need version `3.0.0-beta.1`, which is currently in pre-release, so remember to check “Include prerelease”: ![Find the NuGet package include prelease - How to use biometric authentication in MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/08/image-2.png?resize=640%2C481&ssl=1)Find the NuGet package include prelease Follow the [guide on GitHub](https://github.com/smstuebe/xamarin-fingerprint/tree/maui-support) for how to set it up for your .NET MAUI project. The guide is currently located under the `maui-support` branch, so if the link doesn’t work it’s already been merged in and you can use the previously provided link. Add a button with a click handler. Inside the event handler in the code-behind, add the following code: ``` var request = new AuthenticationRequestConfiguration("Prove you have fingers!", "Because without it you can't have access"); var result = await CrossFingerprint.Current.AuthenticateAsync(request); if (result.Authenticated) { await DisplayAlert("Authenticated!", "Access granted", "Cool beans"); } else { await DisplayAlert("Not authenticated!", "Access denied", "aww"); } ``` Now when you click the button, you will be asked to authenticate yourself via facial recognition or fingerprint. Check the video below to see how it works on Android: ## Abstractions You can also do the same with dependency injection by using the `IFingerprint` interface and resolving it to `CrossFingerprint.Current`. *Note that this may not be best practice when it comes to using DI (dependency injection).* ``` using Plugin.Fingerprint.Abstractions; namespace MauiBiometrics; public partial class MainPage : ContentPage { private readonly IFingerprint fingerprint; public MainPage(IFingerprint fingerprint) { InitializeComponent(); this.fingerprint = fingerprint; } private async void OnCounterClicked(object sender, EventArgs e) { var request = new AuthenticationRequestConfiguration("Prove you have fingers!", "Because without it you can't have access"); var result = await fingerprint.AuthenticateAsync(request); if (result.Authenticated) { await DisplayAlert("Authenticated!", "Access granted", "Cool beans"); } else { await DisplayAlert("Not authenticated!", "Access denied", "aww"); } } } ``` ## iOS Settings As usual iOS and MacCatalyst are different. To have the biometric authentication working on them, you have to open the `Info.plist` under the folders iOS and MacCatalyst and add the following lines ``` NSFaceIDUsageDescription Need your face to unlock secrets! ``` ## Android settings For Android, the only setting we have to change is the **Minimum Target Android Framework** under the **Application** > **Android Targets**. Change the value to 31.0 as the fingerprint component requires. ![Android Minimum Target Android Framework - Use biometric authentication in MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/08/image-3.png?resize=640%2C377&ssl=1)Android Minimum Target Android Framework ## Windows settings For the Windows platform we don’t have to change nothing! When the application will require the biometric authentication, if you have a device that allows to use Windows Hello or another similar authentication, the Windows Security window will appear and check if it is you. ![Windows biometric authentication - Use biometric authentication in MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/08/image-4.png?resize=640%2C398&ssl=1)Windows biometric authentication **Categories:** MAUI **Tags:** biometrics, fingerprints, maui, windows-hello --- ### [Windows under attack by zero-day flaw](https://puresourcecode.com/news/windows-under-attack-by-zero-day-flaw/) **Published:** June 18, 2022 **Author:** Enrico **Excerpt:** Windows under attack by zero-day flaw: the bug has been exploited by Chinese hackers who used it to send malicious documents to Tibetans **Content:** As [Beeping Computer](https://www.bleepingcomputer.com/news/security/microsoft-patches-actively-exploited-follina-windows-zero-day/) reports, the security tweaks bundled in the June 2022 cumulative Windows Updates seal the zero-day security hole that enabled an exploit dubbed Follina ([CVE-2022-30190](https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-30190)). The security flaw came in the form of a Microsoft Windows Support Diagnostic Tool (MSDT) remote code execution bug, which allowed hackers to execute arbitrary code within apps using the tool, and allow the installation of programs, changing or deleting of data or making a new Windows account with a compromised user’s rights on the affected PC. The bug affects machines running Windows 7 or later. And according to security researchers from [Proofpoint](https://twitter.com/threatinsight/status/1531688214993555457), the bug has been exploited by Chinese hackers who used it to send malicious documents to Tibetans. Furthermore, the bug has been used to target U.S. and European Union government agencies. So while you may not be the target of potential state-sponsored hackers, other savvy cyber criminals could look to use the exploit on unpatched PCs to cause havoc. That’s why, like Microsoft, we recommend you make sure your PC is patched as soon as possible. “Microsoft strongly recommends that customers install the updates to be fully protected from the vulnerability. Customers whose systems are configured to receive automatic updates do not need to take any further action,” said Microsoft. So if you have automatic updates enabled then there’s a good chance you’re already protected. But if not you’ll want to ensure you have the latest patch. To do that, head to the Windows Settings app, navigate to the Windows Update section and you should be told if your PC is up to date, if it needs an update, or in some cases if a restart is needed to apply the update. Make sure you do this to help keep your PC protected from malicious and opportunistic hackers. **Categories:** Microsoft, News, Tools, Windows **Tags:** windows-server, windows10, windows11, zero-day --- ### [Internet Explorer is dead](https://puresourcecode.com/news/internet-explorer-is-dead/) **Published:** June 18, 2022 **Author:** Enrico **Excerpt:** After 27 years, Microsoft has finally bid farewell to the Internet Explorer and will redirect Explorer users to the latest version of Edge. **Content:** After 27 years, [Microsoft](https://puresourcecode.com/category/news/microsoft/) has finally bid [farewell to the web browser Internet Explorer](https://puresourcecode.com/news/microsoft-will-kill-off-internet-explorer/) and will redirect Explorer users to the latest version of its [Edge browser](https://puresourcecode.com/?s=edge). As of June 15, Microsoft ended support for Explorer on several versions of [Windows 10](https://puresourcecode.com/tag/windows10/) – meaning no more productivity, reliability or security updates. Explorer will remain a working browser but won’t be protected as new threats emerge. Twenty-seven years is a long time in computing. Many would say this move was long overdue. Explorer has been long outperformed by its competitors, and years of poor user experiences have made it the butt of many internet jokes. For Jung Ki-young, a South Korean software engineer, Microsoft Corp’s decision to retire its Internet Explorer web browser marked the end of a quarter-century love-hate relationship with the technology. To commemorate its demise, he spent a month and 430,000 won ($330) designing and ordering a headstone with Explorer’s “e” logo and the English epitaph: “He was a good tool to download other browsers.” > Someone built a real tombstone of Internet Explorer in Korea. "He was a good tool to download other browsers." [pic.twitter.com/ud3SMiyLNp](https://t.co/ud3SMiyLNp) > > — Soonson Kwon (@ksoonson) [June 15, 2022](https://twitter.com/ksoonson/status/1536938327395680256?ref_src=twsrc%5Etfw) ## How it began Explorer was first introduced in 1995 by the Microsoft Corporation and came bundled with the Windows operating system. To its credit, Explorer introduced many Windows users to the joys of the internet for the first time. After all, it was only in 1993 that Tim Berners-Lee, the father of the web, [released](https://thenextweb.com/news/20-years-ago-today-the-world-wide-web-opened-to-the-public) the first public web browser (aptly called WorldWideWeb). Providing Explorer as its default browser meant a large proportion of Windows’s global user base would not experience an alternative. But this came at a cost, and Microsoft eventually faced multiple [antitrust investigations](https://corporatefinanceinstitute.com/resources/knowledge/strategy/microsoft-antitrust-case/) exploring its monopoly on the browser market. Still, even though [a number](https://www.mozilla.org/en-US/firefox/browsers/browser-history/) of other browsers were around (including Netscape Navigator, which pre-dated Explorer), Explorer remained the default choice for millions of people up until around 2002, when Firefox was launched. ![Internet Explorer is dead](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/internet-explorer-is-dead-2.jpg?resize=640%2C360&ssl=1)Internet Explorer is dead ## How it ended Microsoft has released 11 versions of Explorer (with many minor revisions along the way). It added different functionality and components with each release. Despite this, it lost consumers’ trust due to Explorer’s “legacy architecture” which involved poor [design and slowness](https://www.optimadesign.co.uk/blog/internet-explorer-end-of-life-or-not). It seems Microsoft got so comfortable with its monopoly that it let the quality of its product slide, just as other competitors were entering the battlefield. > is Internet Explorer ever truly dead? [pic.twitter.com/KQGndprUxn](https://t.co/KQGndprUxn) > > — Tom Warren (@tomwarren) [June 14, 2022](https://twitter.com/tomwarren/status/1536687397798350849?ref_src=twsrc%5Etfw) Even just considering its cosmetic interface (what you see and interact with when you visit a website), Explorer could not give users the authentic experience of [modern websites](https://www.techwalla.com/articles/how-to-fix-internet-explorer-pages-not-displaying-correctly). On the security front, Explorer exhibited its [fair share of weaknesses](https://www.cvedetails.com/vulnerability-list/vendor_id-26/product_id-9900/Microsoft-Internet-Explorer.html), which cyber criminals readily and successfully exploited. While Microsoft may have patched many of these weaknesses over different versions of the browser, the underlying architecture is [still considered vulnerable](https://docs.microsoft.com/en-us/deployedge/microsoft-edge-security-iemode-safer-than-ie) by security experts. Microsoft itself has [acknowledged](https://docs.microsoft.com/en-us/deployedge/microsoft-edge-security-iemode-safer-than-ie) this: > … \[Explorer\] is still based on technology that’s 25 years old. It’s a legacy browser that’s architecturally outdated and unable to meet the security challenges of the modern web. These concerns have resulted in the United States [Department for Homeland Security](https://www.dhs.gov/) repeatedly advising internet users against [using Explorer](https://windowsreport.com/internet-explorer-security-issues/). Explorer’s failure to win over modern audiences is further evident through Microsoft’s ongoing attempts to push users towards Edge. Edge was first introduced in 2015, and since then Explorer has only been used as a compatibility solution. ## What Explorer was up against In terms of [market share](https://gs.statcounter.com/browser-market-share#monthly-202206-202206-bar), more than 64% of browser users currently use Chrome. Explorer has dropped to less than 1%, and even Edge only accounts for about 4% of users. What has given Chrome such a leg-up in the browser market? ![While Chrome is dominating the market, Internet Explorer has dropped to less than 1% - Internet Explorer is dead](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-13.png?resize=640%2C503&ssl=1)While Chrome is dominating the market, Internet Explorer has dropped to less than 1% – Source: [Statcounter Global Stats](https://gs.statcounter.com/browser-market-share#yearly-2009-2022-bar) Chrome was first introduced by Google in 2008, on the open source [Chromium project](https://www.chromium.org/chromium-projects/), and has since been actively developed and supported. Being open source means the software is publicly available, and anyone can inspect the source code that runs behind it. Individuals can even contribute to the source code, thereby enhancing the software’s productivity, reliability and security. This was never an option with Explorer. Moreover, Chrome is multi-platform: it can be used in other operating systems such as Linux, MacOS and on mobile devices, and was supporting a range of systems long before Edge was even released. Meanwhile, Explorer has [mainly](https://www.zdnet.com/article/zune-hd-no-youtube-in-the-browser-for-you/) been [restricted](https://docs.microsoft.com/en-us/deployedge/microsoft-edge-supported-operating-systems) to Windows, [XBox](https://puresourcecode.com/?post_tag=xbox) and a few versions of [MacOS](https://puresourcecode.com/?post_tag=macos). ## Under the hood Microsoft’s Edge browser is using the same [Chromium](https://www.chromium.org/chromium-projects/) open-source code that Chrome has used since its inception. This is encouraging, but it remains to be seen how Edge will compete against Chrome and other browsers to win users’ confidence. We won’t be surprised if Microsoft fails to nudge customers towards using Edge as their favourite browser. The latest stats suggest Edge is still far behind Chrome in terms of market share. Also, the fact Microsoft took seven years to retire Explorer after Edge’s initial release suggests the company hasn’t had great success in getting Edge’s uptake rolling. ![Microsoft webpage - Internet Explorer is dead](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-14.png?resize=640%2C266&ssl=1)Microsoft webpage ## What’s next? Web browsers play a vital role in establishing privacy and security for users. Design and convenience are important factors for users when selecting a browser. So ultimately, the browser that can most effectively balance security and ease of use will win users. And it’s hard to say whether Chrome’s current popularity will be sustained over time. Google will no doubt want it to continue, since web browsers are significant [revenue sources](https://fourweekmba.com/how-does-mozilla-make-money/). But Google as a corporation is becoming increasingly unpopular due to massive [data gathering](https://theconversation.com/google-is-leading-a-vast-covert-human-experiment-you-may-be-one-of-the-guinea-pigs-154178) and [intrusive advertising](https://theconversation.com/is-google-getting-worse-increased-advertising-and-algorithm-changes-may-make-it-harder-to-find-what-youre-looking-for-166966) practices. Chrome is a key component of Google’s data-gathering machine, so it’s possible users may slowly turn away. As for what to do about Explorer (if you’re one of the few people that still has it sitting meekly on your desktop) – simply [uninstall](https://docs.microsoft.com/en-us/troubleshoot/developer/browsers/installation/disable-internet-explorer-windows) it to avoid security risks. Even if you’re not using Explorer, just having it installed [could present](https://mashable.com/article/internet-explorer-hacker-windows-pc-exploit) a threat to your device. No one wants to be the victim of a cyber attack via a dead browser! ![Joke about Internet Explorer - Internet Explorer is dead](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-15.png?resize=494%2C538&ssl=1)Joke about Internet Explorer [](https://images.theconversation.com/files/469137/original/file-20220616-13070-5lnc2u.png?ixlib=rb-1.1.0&q=45&auto=format&w=1000&fit=clip) **Categories:** Microsoft, News **Tags:** internet-explorer, microsoft, microsoft-edge --- ### [Create documentation with Docsify and GitHub Pages](https://puresourcecode.com/tools/create-documentation-with-docsify-and-github-pages/) **Published:** June 17, 2022 **Author:** Enrico **Excerpt:** Documentation is an essential part of making any project useful to users. Here how to Create documentation with Docsify and GitHub Pages **Content:** In this tutorial, I’ll show you one how to create nice documentation with [Docsify](https://docsify.js.org/) and [GitHub Pages](https://pages.github.com/). I have created a nice template that is available on [GitHub](https://github.com/erossini/docsify-template). So, documentation is an essential part of [making any project useful](https://puresourcecode.com/dotnet/agile/digital-transformation-scenario-azure-visual-studio-git/) to users. It’s not always developers’ top priority, as they may be more focused on making their application better than on helping people use it. This is why making it easier to publish documentation is so valuable to developers. By default, GitHub Pages prompts users to use [Jekyll](https://docs.github.com/en/github/working-with-github-pages/about-github-pages-and-jekyll). Jekyll is a static site generator that supports HTML, CSS, and other web technologies. Jekyll generates a static website from documentation files encoded in Markdown format, which GitHub automatically recognizes due to their `.md` or `.markdown` extension. While this setup is nice, I wanted to try something else. Fortunately, GitHub Pages’ HTML file support means you can use other site-generation tools to create a website on the platform. Docsify is an MIT-Licensed open-source project with [features](https://docsify.js.org/#/?id=features) that make it easy to create an attractive advanced documentation site on GitHub Pages. ## Get started with Docsify There are two ways to install Docsify: 1. Docsify’s command-line interface (CLI) through NPM 2. Manually by writing your own `index.html` Docsify recommends the NPM approach. If you want to use NPM, follow the instructions in the [quick-start guide](https://docsify.js.org/#/quickstart?id=quick-start). ## Get the template I’ve published this example’s source code on the [project’s GitHub page](https://erossini.github.io/docsify-template/). You can download the files individually or clone the repo with ``` git clone https://github.com/erossini/docsify-template ``` So, in this template you have everything to start with your documentation. The features I added are: - Full index search. This plugin ignores diacritical marks when performing a full text search (e.g., “cafe” will also match “café”). Legacy browsers like IE11 require the following [String.normalize()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize) polyfill library to ignore diacritical marks - Zoom on images - Pagination - Reading progress bar - Mermaid ## Walkthrough the code Now, I walk you through the cloned code from my sample repo below, so you can understand how to modify Docsify. If you prefer, you can start from scratch by creating a new `index.html` file, like in the [example](https://docsify.js.org/#/quickstart?id=manual-initialization) in Docsify’s docs: ``` ``` In this file you can add more plugins and the configuration for the Docsify. To initialize Docsify, you have to add the `script` and the CSS style at least ``` ``` These lines use content delivery network (CDN) URLs to serve the CSS and JavaScript scripts to transform the site into a Docsify site. As long as you include these lines, you can turn your regular GitHub page into a Docsify page. The first line after the `body` tag specifies what to render: ``` ``` Docsify is using the [single page application](https://en.wikipedia.org/wiki/Single-page_application) (SPA) approach to render a requested page instead of refreshing an entirely new page. Last, look at the lines inside the `script` block: ``` window.$docsify = { name: 'Docsify Template', el: "#app", repo: "https://github.com/erossini/docsify-template", loadSidebar: true } ``` In this block: - The `el` property basically says, “Hey, this is the `id` I am looking for, so locate the `id` and render it there.” - Changing the `repo` value identifies which page users will be redirected to when they click the GitHub icon in the top-right corner. - Setting `loadSideBar` to `true` will make Docsify look for the `_sidebar.md` file that contains your navigation links. You can find all the options in the [Configuration](https://docsify.js.org/#/configuration?id=configuration) section of Docsify’s docs. ### Sidebar Next, look at the `_sidebar.md` file. Because you set the `loadSidebar` property value to `true` in `index.html`, Docsify will look for the `_sidebar.md` file and generate the navigation file from its contents. The `_sidebar.md` contents in the sample repo are: ``` * [HOME](./) * [Examples](./examples/index) * [Mermaid](./examples/Mermaid/index.md) - [Flowchart](./examples/Mermaid/flowchart.md) * [📊 Charty](./examples/Charty/index) - [Area](./examples/Charty/area.md) * [About](./about/index) * [Contact](./contact/index) ``` This uses Markdown’s link format to create the navigation. ### README In the root of the folder, you have a `README.md` file. This is the default Markdown file that Docsify reads first. The content is in Markdown format. ## The result Now, the result we expect from this code is what you see in the following screenshot. ![The template runs - Create documentation with Docsify and GitHub Pages](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-5.png?resize=640%2C374&ssl=1)The template runs ## How to run Docsify locally Before continuing to create our documentation with Docsify and GitHub Pages, we want to see the result in our local machines. For that, we have to install Docsify from NPM first running the following command from the Command Prompt (if you are using Windows, don’t use Power Shell or [Windows Terminal](https://puresourcecode.com/tools/windows/windows-terminal-is-here/)) ``` npm i docsify-cli -g ``` This command installs Docsify globally in the NPM. Now, you can start the Docsify server on your local machine using the directory where the project is. For example, in my case I have the project in the folder `C:\Projects\FromGitHub\docsify-template`. So, I run ``` docsify serve C:\Projects\FromGitHub\docsify-template ``` ![Docsify runs in Command Prompt - Create documentation with Docsify and GitHub Pages](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-6.png?resize=640%2C184&ssl=1)Docsify runs in Command Prompt Now, you can open the browser on `https://localhost:54981`. From same reasons, Microsoft Edge doesn’t allow you to open this URL. So, I’m using Firefox and the result is in the following screenshot ![My local Docsify project is running locally - Create documentation with Docsify and GitHub Pages](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-7.png?resize=640%2C403&ssl=1)My local Docsify project is running locally Now, if you change your files or add new one and save, immediately you will see the change in the browser. I found Visual Studio Code fit for this job. ## Enable GitHub Pages Next step in create documentation with Docsify and GitHub Pages is to publish your documents on GitHub. I assume you have already created a repository on GitHub and push your documents on it. Now, click on **Settings** and the **Pages** in your GitHub repository. ![Setting GitHub Pages for your repository](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-9.png?resize=640%2C469&ssl=1)Setting GitHub Pages for your repository So, the first thing to do is to select from the dropdown list the branch that contains the documentation. GitHub advises to create a `docs` branch for it. In my case, I decided to use the `main` brach. Also, it is possible to choose a theme. This is useful only if you don’t use Docsify. ![Select the branch where the documentation is contained](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-10.png?resize=640%2C469&ssl=1)Select the branch where the documentation is contained After this step, GitHub gives you the URL of your documentation. You have to wait at least 15 minutes before GitHub copies the files to the URL. So, if you try immediately to open the URL, you will receive a 404 error. ![GitHub Pages gives you the URL](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-11.png?resize=640%2C541&ssl=1)GitHub Pages gives you the URL ### Custom domain Now, I want to use a custom domain for my documentation. GitHub has this option. The setup is very easy. Add to your DNS the `CNAME` you want and then the main part of the URL. For example, I have a documentation about Agile on but I have to use the custom domain . So, in the PureSourceCode DNS I added: ``` CNAME agile erossini.github.io ``` After this change, GitHub takes at least 15 minutes to recognize the DNS changes. When it finds the change, your GitHub Pages starts to reply to the custom domain. #### Enforce HTTPS If you domain has an SSL certificate, you can ask GitHub to enfors HTTPS for your GitHub Pages. When you set or change your custom domain in the Pages settings, an automatic DNS check begins. This check determines if your DNS settings are configured to allow GitHub to obtain a certificate automatically. If the check is successful, GitHub queues a job to request a TLS certificate from [Let’s Encrypt](https://letsencrypt.org/). On receiving a valid certificate, GitHub automatically uploads it to the servers that handle TLS termination for Pages. When this process completes successfully, a check mark is displayed beside your custom domain name. The process may take some time. If the process has not completed several minutes after you clicked **Save**, try clicking **Remove** next to your custom domain name. Retype the domain name and click **Save** again. This will cancel and restart the provisioning process. **Categories:** Tools **Tags:** documentation, github, github-pages --- ### [Guided Tours Blazor component](https://puresourcecode.com/dotnet/blazor/guided-tours-blazor-component/) **Published:** June 10, 2022 **Author:** Enrico **Excerpt:** I introduce you my new Guided Tours #Blazor component to help your users to understand the UI of your application **Content:** In this new post, I introduce you my new Guided Tours Blazor component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/) to help your users to understand the UI of your application. ## Scenario Have you ever visited a website or a web application, where you don’t have the most remote idea of how to start using it because it’s your first time there? As a web developer, you may want to avoid this experience from your user creating some type of guide video, or a well explained documentation. However, some users won’t read the documentation or see a video because there is no time for that, they just want to use your app! For this kind of users (that’s the 90% of the people) you can use a dynamic in web tour while they learn to use your app. You can implement this feature by using a Guided Tour plugin in your app. ### JavaScript libraries There are some libraries for JavaScript like: - [Anno](https://github.com/iamdanfox/anno.js) is a step-by-step guides plugin for powerful web apps. Anno.js is built to be extensible, the source is about 500 lines of literate coffeescript; you can [read the annotated source](https://iamdanfox.github.io/anno.js/docco/anno.html) in just a few minutes - [jQuery Guide](https://github.com/panlatent/jquery-guide) is a jQuery plugin made to create a “How to use guide” for your web app. It uses jQuery animations to provide a smooth and nice experience for the user while they learn how to use your app - [aSimpleTour](https://github.com/alvaroveliz/aSimpleTour) is a jQuery plugin that will help you to make website tours easily - [Pageguide](https://github.com/tracelytics/pageguide) is a plugin to create interactive guide for web page elements using jQuery and CSS3. Instead of cluttering your interface with static help message, or explanatory text, add a pageguide and let your users learn about new features and functions. Pageguide comes with an example implementation (the files are in /example) which you can run locally with Grunt - [Intro.js](https://github.com/usablica/intro.js) is a Step-by-step guide and feature introduction plugin for your website. When new users visit your website or product you should demonstrate your product features using a step-by-step guide All of them are very nice libraries but there is nothing for Blazor. So, I have created one and the result is here. ## Add Guided Tours to your project First, download and install the package from [NuGet](https://www.nuget.org/packages/PSC.Blazor.Components.Tours/). Then, register the service in your project in 2 ways. ### Startup.cs In your `Startup.cs` you call `UseTour()` function in the `ConfigureServices` like in the following code ``` using PSC.Blazor.Components.Tours; public void ConfigureServices(IServiceCollection services) { // ... services.UseTour(); // ... } ``` ### NET6 configuration If your project is built in NET6, add to your `Program.cs` the call to `UseTour()` like in the following example ``` using PSC.Blazor.Components.Tours; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.UseTour(); await builder.Build().RunAsync(); ``` ### Add style In your `index.html` add the following code to add the generic style in the `HEAD` section of the page ``` ``` Now, the configuration is completed. So, we are ready to create the first tour. ## Create a tour For example, in your page you have a `div` like to following ``` Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ``` Nothing special apart from the `data` tag at the end of the line. The `data` attribute allows the Guided Tours Blazor component to identify the DOM object to highlight and use to add the help balloon. Now, add the code to create the tour. ``` This is my first step ``` It is important to give to the tour a `TourId` that allows you to start the tour. Every tour can have one or more `GuidedTourStep`. Each `GuidedTourStep` has a title, a `StepName` and a `TourStepSequence`. Also, it has an `ElementSelector`: this refers to the `data` attribute in a HTML tag. The component looks this `data` attribute in the page and display the help. Finally, we have to start the tour. For that, add this button ``` Start Tour ``` and the following code ``` @code { [Inject] private ITourService TourService { get; set; } private async Task StartTour() { await TourService.StartTour("FormGuidedTour"); } } ``` ![Basic Guided Tour with the Blazor component - Guided Tours Blazor component](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/173084399-67c077ec-89ec-44fc-ad51-170cf62a2cf5.gif?w=640&ssl=1)Basic Guided Tour with the Blazor component ## Guided Tour Component The Guided Tour component is the main visual component that displays a tour on the page. Multiple tours can be registered as long as they have unique Id’s. The GTour component inherits from a base component that in turn implements an interface to ease development and extensibility. ### Adding a Tour To add a tour to a razor page, import the namespace if not globally imported and add the component to the page. Below is an example of all the properties available on the tour component. The Arrow Element details can be found on the respective help page. The child content holds the tour steps and can be found on the respective page. ``` ... ... ``` ### Properties - `TourId`: The unique identifier of the tour. - `ArrowClass`: An override class name to style the arrow - `CloseOnOverlayClick`: A value to indicate if the tour should be stopped when clicking on the overlay - `HighlightClass`: A class to be appended to a Tour Step Element that is “Focused”. - `HighlightEnabled`: A value to indicate if the highlight class should be appended to the focused element - `OverlayClass`: An additional class to append to the overlay - `OverlayEnabled`: A value indicating if the overlay is enabled for the tour - `ShowArrow`: A value to indicate if the arrow should be rendered for the tour component - `TourWrapperClass`: An additional class to be appended to the root tour element ### Callbacks - `OnTourCanceled`: A callback for when the tour is canceled - `OnTourCompleted`: A callback for when the tour is completed - `OnTourStarted`: A callback for when the tour has started - `OnTourStepRegistered`: A callback for when a child tour step has been registered - `OnTourStepDeRegistered`: A callback for when a child tour step has been deregistered ### Adding a Tour Step To add a tour step to a razor page, create a new component in the Guided Tour `ChildContent` tag. Even though the sequence is not required to be set is highly advisable that this property be set. The tour step name is also a required property that needs to be set. ``` My Header ... First Step Prev Complete ``` ### Properties - `StepName`: The unique name of the step - `TourStepSequence`: The sequence of this step within the tour - `ElementSelector`: Optional element selector for this step, this will set where this step is displayed - `CancelTourButtonClass`: An additional class to append to the default cancel tour button - `CancelTourButtonText`: The text for the cancel tour button - `CanCancelTour`: A value indicating if this step can cancel the tour - `ChildContent`: Custom content to render for the child step. The @context variable can be used to reference the tour step - `CompleteTourButtonText`: The text for the complete tour button - `CompleteTourButtonClass`: An additional class to append to the default complete tour button - `ContentClass`: An additional class to append to the content wrapper - `FooterClass`: An additional class to append to the footer wrapper. - `FooterContent`: Custom content to render as the footer. The @context variable can be used to reference the tour step - `HeaderClass`: An additional class to append to the header wrapper - `HeaderContent`: Custom content to be rendered in the header. The @context variable can be used to reference the tour step - `NextStepButtonClass`: An additional class to append to the default next step button - `NextStepButtonText`: The text for the next step button - `PopupPlacement`: The position of the tour step - `PopupStrategy`: Can either be absolute or fixed - `PreviousStepButtonClass`: An additional class to append to the default previous step button - `PreviousStepButtonText`: The text for the previous step button - `SkipStep`: A value indicating if the step should be skipped - `Title`: If a custom header is not supplied, this will be the title of the step - `WrapperClass`: An additional class to append to the main wrapper ### Callbacks - `OnNavigateNext`: A callback for the tour is navigating to the next step - `OnNavigatePrevious`: A callback for the tour is navigating to the previous step - `OnStepActivated`: A callback for when the tour has been activated - `OnStepDeActivated`: A callback for when this step has been de-activated - `OnTourCanceled`: A callback for when the tour is canceled - `OnTourCompleted`: A callback for when the tour is completed ## Tour Service The **TourService** is registered as a singleton service and can only have one active tour started at any time. When the **StartTour** method is called while there is an existing tour active, the first tour will be stopped first. Depending on what step the tour is on, this can be either **TourCompleted** or **TourCanceled**. If the tour is on the last step the **TourCompleted** will be called, if not the **TourCanceled** will be called. The service is registered in the Dependency Injection Container on startup. See the example below to register the service. ``` public void ConfigureServices(IServiceCollection services) { // ... services.UseTour(); // ... } ``` ### Methods The **TourService** has the following methods available and the tour can be controlled from this point. This is especially helpful when navigating from custom content in the tour steps. - RegisterTour(ITour gTour)*This is called internally and will automatically be called once a tour is included on a razor page.* - StartTour(string tourId, string startStepName = default)*Tries to start a tour with the given name and optionally the first step of the tour* - StartTour(ITour gTour, string startStepName = default)*Tries to start a tour with the interface reference and optionally the first step of the tour* - StopTour()*Stops the currently active tour. If the Tour is on the last step, the tour will be completed. If not it will be canceled* - CancelTour()*Cancels the current active tour* - PreviousStep()*Navigates to the previous step in the active tour if there is a step prior* - GoToStep(string stepName)*Navigates to the step if it exists* - CompleteTour()*Completes the tour* - DeRegisterTour(ITour gTour)*This is called internally and will automatically be called once a tour component is disposed.* ``` ... ... Complete ``` ``` [Inject] private ITourService TourService { get; set; } private async Task StartTour() { // ... await TourService.StartTour("TourName", "startAtStep"); // ... } ``` ## Events The **TourService** has a few events that can be used to help drive the tour as well as assist in logging output. - OnTourRegistered - OnTourDeRegistered - OnTourStarting - OnTourStarted - OnTourCanceling - OnTourCanceled - OnTourCompleting - OnTourCompleted ``` @implements IDisposable protected override void OnInitialized() { base.OnInitialized(); TourService.OnTourRegistered += OnTourRegistered; } private void OnTourRegistered(ITourService sender, ITour tour) { Console.WriteLine($"Tour with name {tour.TourId} registered"); } public void Dispose() { TourService.OnTourRegistered -= OnTourRegistered; } ``` ## Arrows An arrow indicator has been included on the Tour Steps. It looks ugly yeah but the arrow should rather be an icon element or not show at all. So this is how you do it. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-2.png?resize=640%2C168&ssl=1) ### No Arrow To turn of the Arrow Element rendering, set the Show Arrow Element on the guided tour ``` ``` ### Default Arrow Style This is the default style applied to the arrow and can be overwritten by either overwriting the classes or setting the `ArrowClass` property on the Guided Tour. ``` .arrow, .arrow::before { position: absolute; width: 8px; height: 8px; background: inherit; } .arrow { visibility: hidden; &.force-hide::before { visibility: hidden !important; } } .arrow::before { background: white; visibility: visible; content: ''; transform: rotate(45deg); &.force-hide { visibility: hidden !important; } } ``` ``` ``` ### Custom Icon This is the prefered method of showing the arrow element. Take not of the data attribute **data-popper-arrow**. This allows `popperJs` to pick up the arrow element and position it accordingly. In the below sample a bootstrap icon is used as the arrow element. This is accompanied with a style to rotate the icon element. ``` ``` ``` .guided-tour-wrapper[data-popper-placement^='top'] > .my-arrow { bottom: -16px; transform: rotate(180deg) !important; } .guided-tour-wrapper[data-popper-placement^='bottom'] > .my-arrow { top: -12px; } .guided-tour-wrapper[data-popper-placement^='left'] > .my-arrow { right: -12px; transform: rotate(90deg) !important; } .guided-tour-wrapper[data-popper-placement^='right'] > .my-arrow { left: -12px; transform: rotate(-90deg) !important; } ``` ## Overlay An overlay can be displayed when the tour is active, by default this is on. There is also an option to stop the tour when the overlay is clicked. The overlay element class can also be set with the OverlayClass property. ### Disabling the Overlay The overlay can be disabled by setting the `OverlayEnabled` property on the Tour. ``` ``` ![Disabling the Overlay](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-3.png?resize=640%2C114&ssl=1)Disabling the Overlay ### Close on Click To close the tour when the overlay is clicked, set the **CloseOnOverlayClick** to true. If the tour is on the last step, it will internally call the `TourCompleted` action and if not, it will call the `TourCancelled` action. ``` ``` ![Close on Click](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/image-4.png?resize=640%2C131&ssl=1)Close on Click ## Theming The Guided Tour package comes with 3 built in themes. It also allows for custom themes to be built. - None – This will apply a blank theme to the component and all styles should be set. - Default – This is the default built in theme. - Bootstrap – Bootstrap classes are used for the theme. ### Adding a Custom Theme Create a new theme class and inherit from the **ITheme**. Implement the class names to use. Below is an example of the Bootstrap implementation. ``` public class MyOwnCustomTheme : ITheme { public string TourOverlay { get; set; } public string TourWrapper { get; set; } public string TourArrow { get; set; } public string TourStepWrapper { get; set; } = "modal-content "; public string TourStepHeaderWrapper { get; set; } = "modal-header "; public string TourStepContentWrapper { get; set; } = "modal-body "; public string TourStepFooterWrapper { get; set; } = "modal-footer "; public string TourStepHeaderTitle { get; set; } = "modal-title "; public string TourStepCancelButton { get; set; } = "btn btn-warning "; public string TourStepPreviousButton { get; set; } = "btn btn-secondary "; public string TourStepNextButton { get; set; } = "btn btn-primary "; public string TourStepCompleteButton { get; set; } = "btn btn-success "; } ``` **Categories:** .NET6, Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly **Hashtags:** blazor, blazor-component --- ### [New Profiler feature in Visual Studio](https://puresourcecode.com/news/new-profiler-feature-in-visual-studio/) **Published:** June 10, 2022 **Author:** Enrico **Excerpt:** Microsoft launched a new profiling tool in Visual Studio 17.2 that helps you understand how you can optimize your File I/O operations **Content:** Microsoft just launched a **new profiling tool** in Visual Studio 17.2 that helps you understand how you can optimize your File I/O operations to improve performance in your apps. If you’re trying to investigate and diagnose slow loading times, the new **File IO tool** can help you understand how the I/O operations impact your spent time. ### How to use File IO [](https://devblogs.microsoft.com/visualstudio/new-profiler-feature-in-visual-studio/#how-to-use-file-io) - Select **Alt+F2** to open the Performance Profiler in Visual Studio. - Select the **File IO** check box along with any other cooperative tools you might need. [![Analysis Target - ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/file-io-launch-1024x593.png?resize=640%2C371&ssl=1)](https://i0.wp.com/devblogs.microsoft.com/visualstudio/wp-content/uploads/sites/4/2022/06/file-io-launch.png?ssl=1)Analysis Target - Select **Start** to run the tool. - Select **Yes**. - After the tool starts running, go through the scenario you want to profile in your app. Then select **Stop collection** or close the app to see your data. ## View file read and write information to improve perf [](https://devblogs.microsoft.com/visualstudio/new-profiler-feature-in-visual-studio/#view-file-read-and-write-information-to-improve-perf) The File IO tool provides file read and write information with files read during the profiling session and can help you diagnose performance issues such as inefficient data read or write patterns. The files are autogenerated in a report after collection and arranged by their target process with aggregate information displayed. [![Analysis Target Report](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/file-io-after-1024x303.png?resize=640%2C189&ssl=1)](https://i0.wp.com/devblogs.microsoft.com/visualstudio/wp-content/uploads/sites/4/2022/06/file-io-after.png?ssl=1)Analysis Target Report If you right-click on one of the rows, you can go to the source in your code. If an aggregate row was read multiple times, expand it to see the individual read operations for that file with its frequency, if they were read multiple times. [![Calls from your application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/2022-05-23_23-44-02-1024x341.png?resize=640%2C213&ssl=1)](https://i0.wp.com/devblogs.microsoft.com/visualstudio/wp-content/uploads/sites/4/2022/06/2022-05-23_23-44-02.png?ssl=1)Calls from your application ### Duplication Factor [](https://devblogs.microsoft.com/visualstudio/new-profiler-feature-in-visual-studio/#duplication-factor) The best part of the File IO tool is the **Duplication Factor** feature because it can help you make informed decisions about where you can reduce the read or processing time. Duplication Factor shows if you’re reading or writing more than what you need to from the file. If you have a duplication factor of 3x, that means the number of bytes you’re reading from the file is 3 times the size of the file itself, which may be an indication that you’re reading, and processing more than you realized. This can indicate a place where caching the result of the file read and processing could improve your app’s performance. 🔥 [![Duplication Factor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/file-io-duplication-factor-1024x453.png?resize=640%2C283&ssl=1)](https://i0.wp.com/devblogs.microsoft.com/visualstudio/wp-content/uploads/sites/4/2022/06/file-io-duplication-factor.png?ssl=1)Duplication Factor ### Backtraces view [](https://devblogs.microsoft.com/visualstudio/new-profiler-feature-in-visual-studio/#backtraces-view) Double-clicking any file will cause it to be loaded in the **Backtraces** view. This view loads for any file in either reads or writes, allowing you to see where the read or write is happening in your code. [![Backtraces view ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/file-io-backtraces-1024x307.png?resize=640%2C192&ssl=1)](https://i0.wp.com/devblogs.microsoft.com/visualstudio/wp-content/uploads/sites/4/2022/06/file-io-backtraces.png?ssl=1)Backtraces view **Categories:** Microsoft, News, Tools, Visual Studio **Tags:** profiler, visualstudio-2022 --- ### [Clippy Blazor component](https://puresourcecode.com/dotnet/blazor/clippy-blazor-component/) **Published:** June 10, 2022 **Author:** Enrico **Excerpt:** I introduce you my new Clippy Blazor component for Blazor WebAssembly and Blazor Server. The component is built with .NET6. **Content:** In this new post, I introduce you my new Clippy Blazor component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). The component is built with .NET6. Some useful link: - Source code available on [GitHub](https://github.com/erossini/BlazorClippy) - Demo website [here](https://clippy.puresourcecode.com/) ## Who is Clippy? Do you remember Clippy? If the answer is no, please go away! This is a component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webass9embly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). The components is build with .NET6. The demo application is available [here](https://clippy.puresourcecode.com/). Clippy is an Office assistant that helped users when they were using any of the Microsoft Office applications. Clippy’s role was to communicate with users and give corresponding actions. The original name was Clippit, but it was later nicknamed “Clippy”. The name came as a result of its resemblance to a paperclip. It was one of the notable assistants that helped users when using any MS applications. Unfortunately, Clippy got some negative feedback from certain users that led to its removal in later Microsoft Office applications versions. Clippy is a paperclip that was created by Kevan J. Atterberry. It was made to create a social interface to make it easier for people to understand the computer. Generally, the idea behind Clippy was to assist users in understanding how to use the operating system. Thereby, users could access certain features on Microsoft programs and applications quickly. The Clippy used a series of Bayesian algorithms to determine the users’ needs. In a big way, it also helped in typing cues, autoformat, and other features. However, people later complained that it was intrusive ad annoying, which led to its removal. ## Add Clippy library In your Blazor project, add the Clippy package from [Nuget](https://www.nuget.org/packages/PSC.Blazor.Components.Clippy/). In the `Import.razor` add the reference to the package ``` @using PSC.Blazor.Components.Clippy ``` Then, you have to add the **Clippy** service to your project. In your `Program.cs`, add the following line ``` using PSC.Blazor.Components.Clippy ... builder.Services.AddScoped(); ``` before ``` await builder.Build().RunAsync(); ``` ### Add scripts Now, you have to add the CSS style and the scripts for Clippy. Go to your `index.html` under `wwwroot`. In the `HEAD` section, add the folloing line ``` ``` Then, at the bottom of the page before closing the tag `BODY` add the following scripts: ``` ``` If your application doesn’t have `jQuery`, you have to add this line ``` ``` ## Use Clippy To use Clippy in a Razor page, inject `ClippyService` in the page adding this code in the `@code` section ``` [Inject] public ClippyService clippy { get; set; } ``` or at the top of the page ``` @inject ClippyService clippy ``` Now, you can use `clippy` service in the page. To load an agent, use this code ``` await clippy.Load(agentName); ``` `agentName` is one of the following from the enum `AgentName` - Bonzi - Clippy (default) - F1 - Genie - Genius - Links - Merlin - Peedy - Rocky - Rover ![Available agent in Clippy Blazor component - Clippy Blazor component](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/agents-for-clippy.png?resize=640%2C386&ssl=1)Available agent in Clippy Blazor component ## Clippy functions NameParametersReturnDescriptionAnimateRandomPlay an randon animation for the listGestureAtint x, int yThe agent points in the direction of the x and yGetAnimationsListListList of animation for an agentHideHide and dispose the agentLoadAgentName agentNameLoad an agent. By default the agent is `Clippy`. Select another agent using the enum `AgentName`PlayAnimationClippyAnimations animationPlay one of the animation for the agent. Choose from the enum `ClippyAnimations`. See below the list of available animationsSpeakstring textShow the `text` in the baloonStopStop all actions in the queue and go back to idle modeStopCurrentStop the current action in the queue## [](https://github.com/erossini/BlazorClippy#animation-list)Animation list - Congratulate - LookRight - SendMail - Thinking - Explain - IdleRopePile - IdleAtom - Print - Hide - GetAttention - Save - GetTechy - GestureUp - Idle1\_1 - Processing - Alert - LookUpRight - IdleSideToSide - GoodBye - LookLeft - IdleHeadScratch - LookUpLeft - CheckingSomething - Hearing\_1 - GetWizardy - IdleFingerTap - GestureLeft - Wave - GestureRight - Writing - IdleSnooze - LookDownRight - GetArtsy - Show - LookDown - Searching - EmptyTrash - Greeting - LookUp - GestureDown - RestPose - IdleEyeBrowRaise - LookDownLeft ## [](https://github.com/erossini/BlazorClippy#example)Example ``` await clippy.Load(AgentName.Clippy); await clippy.PlayAnimation(ClippyAnimations.GetAttention); await clippy.Speak("Hello Blazor! Do you like it?"); await clippy.Speak("I can help your users. You know I can do that :)"); await clippy.PlayAnimation(ClippyAnimations.CheckingSomething); await clippy.Speak("I helped whole generation of people with PC :)"); await clippy.PlayAnimation(ClippyAnimations.GoodBye); ``` ## [](https://github.com/erossini/BlazorClippy#screenshot-demo)Screenshot demo The demo application shows all the functionalities offer from the component. Select an agent from the list and then click *Load*. ![Demo screenshot - Clippy Blazor component](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/Screenshot1.png?resize=640%2C443&ssl=1)Demo screenshot Here the list of agents available. ![Demo screenshot - Clippy Blazor component](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/06/Screenshot2.png?resize=640%2C443&ssl=1)Demo screenshot **Categories:** .NET6, Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly, clippy, microsoft-office, net6 --- ### [.NET is the most loved platform](https://puresourcecode.com/dotnet/net-core/net-is-the-most-loved-platform/) **Published:** May 27, 2022 **Author:** Enrico **Excerpt:** .NET is the most loved platform! It’s won most loved platform 3 years in a row in the annual StackOverflow developer survey **Content:** In the annual Stackoverflow survey, .NET is the most loved platform from developers and the public! As someone who has built more than one popular platform on [.NET](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet) (DotNET for the marketing conscious), I often get asked about its relevance and whether it’s an ecosystem really worth investing in. This question is especially popular among folks living in the tech distortion field in the San Francisco Bay Area where tech fads come and go like the changing seasons, but [.NET](https://puresourcecode.com/tag/net6/) remains steadfast as not only a consistently popular platform, but as far as I’m concerned, the most holistically productive, delightful, and accessible platform there is. Yes, there are other fantastic languages out there; I see you, Rust! Yes there are other fantastic App UX platforms, hello, Flutter, you’re gorgeous. But for all-round productivity and elegance, nothing else stacks up. The .NET today is not your parent’s .NET, and there’s a reason why **it’s won most loved platform 3 years in a row in the annual StackOverflow developer survey**. In fact, combining .NET Framework (a little older), with the new .NET Core stuff, it blows everything else out of the water by a longshot: ![Stackoverflow developer survey - .NET is the most loved platform](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-10.png?resize=640%2C641&ssl=1)Stackoverflow developer survey Perhaps even just as telling; .NET also shows the strongest positive sentiment of love vs. dread of any framework in the survey. To understand why developers love .NET so much, let’s examine the .NET experience holistically. ## The tooling is best-in-class First up is the tooling; .NET enjoys first class support in Visual Studio for Windows, Mac, and limited support in VS Code (an omission on Microsoft’s part that we’ll discuss below). Together, they represent the most utilized IDEs in the world, in fact VS Code is the absolute reigning monarch of IDEs, used by over twice the number of folks as the second most utilized IDE (Visual Studio, of course): ![Visual Studio Core and Visual Studio are the best - .NET is the most loved platform](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-11.png?resize=640%2C234&ssl=1)Visual Studio Core and Visual Studio are the best ## It runs everywhere. Much like the 1990s dream of Java, .NET runs on every major platform out there, and has custom, integrated tooling to take advantage of each platform on most. While it was first locked on Windows, today, it’s at home on Windows, macOS, iOS, Android, Linux, Mainframes, and even microcontrollers. It also runs natively in the cloud; Azure, AWS, and Google Cloud all have built-in .NET application support. ## There’s a spectrum of elegant languages to choose from. ![logos of F#, C#, and VB.NET - .NET is the most loved platform](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/img_62907b95e1878.png?w=640&ssl=1)F# C# and Visual Basic A core feature of .NET is the Common-Language-Runtime (CLR), which enables applications to be written in [over 25 currently maintained languages](https://en.wikipedia.org/wiki/List_of_CLI_languages), including C#, and F#; two of the finest languages on the planet, as well as VB.NET; which while being a favorite language to poke fun at, is incredibly powerful in its own right. ## There is a massive community and skilled labor-force Depending on how you count, there are somewhere between 20–25MM³ active developers. By our own research, at somewhere north of 10MM, **nearly half the active devs in the world either know, or actively develop in .NET**. And that ecosystem of developers has grown every single year since the inception of .NET. So at a time when it’s notoriously difficult to hire developers, you can rest assured that .NET enjoys one of the largest development talent pools to pull from. It’s still growing fast, too; the latest TIOBE Programming Language Index, which indicates the overall popularity of a language shows that **over the last year, C# has seen, by far, the largest increase in popularity and is expected to enter the top three, replacing C⁴**. In fact, when combined with VB.NET, it easily ties for first place. ## It’s technically beautiful. 2016’s .NET Core remodel stripped .NET down to its bare bones and created a modern, ultralightweight and composable, a la carte way to consume the minimum necessary platform libraries directly from its modern package ecosystem, Nuget. It also dropped the legacy Win32 platform connections, paving the way for a truly cross-platform experience as it merged the innovations that the Mono and Xamarin team brought to the runtime and tooling. .NET Core also enabled it to get a whole lot more performant; by breaking from the legacy .NET Framework runtime constraints, Microsoft was able to include a litany of performance improvements across the board. And speaking of performance, Xamarin’s innovative Ahead-of-Time compiler (now mainlined into the unified .NET) means that developers can ship binaries that are compiled all the way down to chip architecture specific assembly code at build-time, enabling native C/C++ performance from memory-managed applications. That intrinsic memory-managed approach pays dividends in reliability and security too. From a reliability perspective, it virtually eliminates the instability and crashes due to memory leaks that are so common in unmanaged applications such as those written in C/C++. From a security standpoint, the memory safety achieved from a memory-managed platform eliminates a whole swath of security issues due to memory usage which, according to Google, accounts for roughly 2/3rds of all unmanaged security bugs⁵. ## It’s 100% open-source. ![GitHub (logo) + .NET (logo) = heart emoji - .NET is the most loved platform](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/img_62907b96a7986.png?w=640&ssl=1)GitHub + .NET All aspects of Microsoft’s .NET are open source and free to use, including class libraries, runtime, compilers, languages, and application frameworks⁶. You can find the source code all in one place on GitHub, in the [DotNet](https://github.com/dotnet) repository. **Categories:** .NET, .NET Core, .NET General, .NET5, .NET6, Microsoft, News **Tags:** net6, netcore, stackoverflow, survey, visual-studio --- ### [Call API Management from Blazor](https://puresourcecode.com/dotnet/net-core/call-api-management-from-blazor/) **Published:** May 20, 2022 **Author:** Enrico **Content:** The title ”Call API Management from Blazor” is not explain fully what I’m going to explain in this post but it is only a title. So, consider the following scenario. ## Scenario On [Azure API Management Service](https://puresourcecode.com/tag/azure-api-manager/) you have your APIs. For more protection, you want to add another level of security asking to the API Management to validate the user token for each request. The token is validated again your own [Identity Server](https://puresourcecode.com/tag/identityserver4/). Once the API Management is configured to use Identity Server for the validation, you want to call the APIs from a [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) application. So, the first issue you will face is how to read the user token with Blazor and then add the `Authorization` to the `HttpClient` request. Now, I must confess I spent almost 2 weeks to find a solution to all of this. I hope this post could be useful for someone else. Before starting to read this post, I recommend to read my other posts about the API Management Service: - [How to use an Azure API Management Service](https://puresourcecode.com/tools/azure-tools/how-to-use-an-azure-api-management-service/) - [Configure CORS in API Management](https://puresourcecode.com/tools/azure-tools/configure-cors-in-api-management/) ## Configure OpenID Connect So, first step is to configure the OpenID connect on the API Management Service on [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/). For that, go to the resource and on the menu on the left, select **OAuth 2.0 + OpenID Connect**. ![Add OpenID to the API Management Service - Call API Management from Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-4.png?resize=640%2C316&ssl=1)Add OpenID to the API Management Service Then, at the top click on **Add** to add a new configuration.Now, type the **Display name** and the **Name** you want to use, the Description and the **Metadata endpoint URL**. If you are using the [Identity Server](https://puresourcecode.com/dotnet/net-core/implement-security-workflow-with-identity-server/) and the [Skoruba Admin UI](https://github.com/skoruba/Duende.IdentityServer.Admin), click on **Discovery Document** to obtain the URL. ![Skoruba Admin UI - Discovery Document - Call API Management from Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-5.png?resize=640%2C354&ssl=1)Skoruba Admin UI – Discovery Document Then, **Client ID** and **Client Secret** are required. Again, you can use Skoruba Admin UI to create them. The configuration is quite simple: - Require Client Secret - Allow Offline Access - Allow Access Token Via Browser - Allowed scopes: - openid - profile - roles - email - Allowed Grant types - client\_credentials - implicit - authorization\_code - password - hybrid Then, set a **Client Secret**. If you use the code to configure the IdentityServer use the following: ``` new Client { ClientId = "...", ClientName = "...", AllowedGrantTypes = GrantTypes.HybridAndClientCredentials, ClientSecrets = { new Secret("secret".Sha256()) }, RedirectUris = { "https://local:44352/signin-oidc" }, PostLogoutRedirectUris = { "https://local:44352/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile }, AllowOfflineAccess = true, AllowAccessTokensViaBrowser = true, } ``` Now, that this configuration is done, we have to add the IdentityServer to the API. ## Configure the Security of the APIs So, I assume that you have already created the APIs in the API Management Service. Now, click on **Settings** of the API and scroll down in the section. ![Security in the API Management Service - Call API Management from Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-6.png?resize=541%2C146&ssl=1)Security in the API Management Service Here, select **OpenID connect** and select from the dropdown list your **Identity Server**. Then, press the **Save** button. ## Configure the Design of the APIs Next step is to validate the request. For that, we want to be sure that the request contains a valid **JWT** token. The token is generate from the Identity Server for each session and user when the user logs in the application. When the application calls the API, the `HttpClient` has to include in the header this user token. So, I have to configure the Inbound processing to validate the JWT token and only if it is valid, the API Manager proceeds with the request. In the other case, the API Manager returns an `HTTP 401`. Now, click on **All operations** and then open the API **design**. ![API Management Service - API Design](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-7.png?resize=640%2C391&ssl=1)API Management Service – API Design Now, **Add policy** and I have the following screen. ![Add inbound policy](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-8.png?resize=640%2C439&ssl=1)Add inbound policy Select **Validate JWT**. Now, the configuration is: - **Validate by**: Header - **Header name**: Authorization - **Failed validation HTTP code**: 401 – Unauthorized - **Open ID Urls**: https://youridsrv/.well-known/openid-configuration ![Validate JWT](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/05/image-9.png?resize=640%2C827&ssl=1)Validate JWT Also, as a reminder, you have to configure the **CORS**. To add a bit more security to the output of your API, I like to set some parameters (see my other [post about it](https://puresourcecode.com/dotnet/blazor/add-security-headers-to-blazor-webassembly/)). Then, the **Policies** of the API looks like the following configuration: ``` * GET POST * * max-age=31536000 1; mode=block script-src 'self' deny nosniff max-age=604800,enforce none ``` ### Redirect all HTTP request to HTTPS So, Microsoft says that currently API Management Service doesn’t support [HSTS header](https://puresourcecode.com/dotnet/blazor/add-security-headers-to-blazor-webassembly/). You can configure each API to listen on `HTTP`, `HTTPS` or both but this does not support redirection, if you configure an API to only listen on https and sends http request you will get 404 from API Management. However, you can add an input policy which can redirect all `HTTP` calls to `HTTPS`. This is the most recommended approach. ``` @("https://" + context.Request.OriginalUrl.Host + context.Request.OriginalUrl.Path) ``` ## Configure Blazor Now, it is time to add some configuration to the Blazor project. The main problem in Blazor is how to have access to the user token. If you google, you find a lot of solutions and most of them are quite complicated. Fortunately, Microsoft gives us, recently, a quite simple way. In the API Service, I have to inject `IAccessTokenProvider` that allows me to read the token. First, the namespaces we have to use. Thanks to NET6, I can create a `GlobalUsing.cs` to add all the packages I want to use across the project. ``` global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Components.Web; global using Microsoft.AspNetCore.Components.WebAssembly.Authentication; global using Microsoft.AspNetCore.Components.WebAssembly.Hosting; global using Microsoft.Extensions.Configuration; ``` ### Appsettings.json Now, I want to save the settings for the API URL and the **HttpClient** settings in the `appsettings.json`. So, my settings is like that ``` { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "oidc": { "Authority": "https://your-identity-server-url/", "ClientId": "220005UI", "ResponseType": "code", "DefaultScopes": [ "openid", "profile", "roles", "email", "offline_access", "220005_api" ], "PostLogoutRedirectUri": "authentication/logout-callback", "RedirectUri": "authentication/login-callback" }, "Api": { "EndpointsUrl": "https://api.psc.com/5/v1/", "Scope": "my_api" }, "ApplicationSettings": { "AuthorizedUrls": [ "https://localhost:7241" ], "Scopes": [ "my_api" ], "SubscriptionKey": "3251" } } ``` The `oidc` configuration is for the connection with Identity Server as required for the `AddOidcAuthentication`. To recognise the user’s roles, I’m using my package [PSC.Blazor.AuthExtensions](https://www.nuget.org/packages/PSC.Blazor.AuthExtensions/). To read the configuration for the application, this is model called `ApplicationSettingsModel` ``` public class ApplicationSettingsModel { public Applicationsettings ApplicationSettings { get; set; } public string SubscriptionKey { get; set; } } public class Applicationsettings { public string[] AuthorizedUrls { get; set; } public string[] Scopes { get; set; } } ``` ### Program.cs Now, I can change the Program.cs to read the configuration and set up the `HttpClient` correctly. ``` var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); #region Read configuration string apiEndpoint = builder.Configuration["Api:EndpointsUrl"]; string apiScope = builder.Configuration["Api:Scope"]; ApplicationSettingsModel settings = new ApplicationSettingsModel(); builder.Configuration.Bind(settings); #endregion #region Dependecy injection builder.Services.AddTransient(_ => { return builder.Configuration.GetSection("ApplicationSettings").Get(); }); builder.Services.AddScoped(); #endregion #region Configure HTTP Client builder.Services.AddHttpClient("myAPI", cl => { cl.BaseAddress = new Uri(apiEndpoint); }) .AddHttpMessageHandler(sp => { var handler = sp.GetService() .ConfigureHandler( authorizedUrls: settings.ApplicationSettings.AuthorizedUrls, scopes: settings.ApplicationSettings.Scopes ); return handler; }); builder.Services.AddScoped(sp => sp.GetService().CreateClient("myAPI")); #endregion #region Configure Authentication and Authorization builder.Services.AddOidcAuthentication(options => { builder.Configuration.Bind("oidc", options.ProviderOptions); options.UserOptions.RoleClaim = "role"; }) .AddAccountClaimsPrincipalFactory(); builder.Services.AddAuthorizationCore(); #endregion await builder.Build().RunAsync(); ``` The configuration of the `HttpClient` uses `AddHttpMessageHandler` that coming from the `Microsoft Authorization` namespace. ## Create the API service with authentication Finally, the API Service implementation. Here, in the constructor we need the `IAccessTokenProvider` to obtain the user’s token. Also, I inject the `ApplicationSettingsModel` to have the configuration I need. ``` public class APIService { private readonly HttpClient _httpClient; private readonly IAccessTokenProvider _accessToken; private readonly JsonSerializerOptions _options; public APIService(HttpClient httpClient, IAccessTokenProvider accessToken, ApplicationSettingsModel settings) { _httpClient = httpClient; _httpClient.DefaultRequestHeaders.Add("Cache-Control", "no-cache"); _httpClient.DefaultRequestHeaders.Add("Apim-Subscription-Key", settings.SubscriptionKey); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; _accessToken = accessToken; } public async Task GetAttributeAsync(APIRequest apirequest) { try { var tokenResult = await _accessToken.RequestAccessToken(); tokenResult.TryGetToken(out var token); HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"yourAPI"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Value); var content = new StringContent(JsonSerializer.Serialize(apirequest), Encoding.UTF8, "application/json"); request.Content = content; HttpResponseMessage responseMessage; responseMessage = await _httpClient.SendAsync(request); responseMessage.EnsureSuccessStatusCode(); if (responseMessage.IsSuccessStatusCode) { var responseContent = await responseMessage.Content.ReadAsStringAsync(); #if DEBUG Console.WriteLine("[GetAttributeAsync] API Response: " + responseContent); #endif return JsonSerializer.Deserialize(responseContent, _options); } else return new APIResponse() { Success = false }; } catch (Exception ex) { return new APIResponse() { Success = false }; } } } ``` The important part is how I read the token. The code is here ``` var tokenResult = await _accessToken.RequestAccessToken(); tokenResult.TryGetToken(out var token); ``` Using the instance of `IAccessTokenProvider`, I can read the user’s token and pass it in the header of the `HttpClient`. So, now we can test the solution. It works in my end! 😁 ## Wrap up In conclusion, in this post I showed how to call an Azure API Management protected with Identity Server from Blazor. I sent a lot of time to sort out how to do it. I hope this code it is useful to someone else! Happy coding! **Categories:** .NET Core, .NET6, Azure, Azure, Blazor, Tools **Tags:** azure, azure-api-manager, blazor, blazor-server, blazor-webassembly, identityserver4 --- ### [Start with MAUI](https://puresourcecode.com/dotnet/net-core/start-with-maui/) **Published:** April 26, 2022 **Author:** Enrico **Excerpt:** It is time to start with MAUI. With the Release Candidate from yesterday we can update Visual Studio 2022 Preview to play with Multi-platform **Content:** It is time to start with MAUI! After the [Release Candidate](https://puresourcecode.com/dotnet/maui/maui-release-candidate-is-here/) from yesterday, we can update Visual Studio 2022 Preview to play with **Multi-platform App UI (MAUI)**. ## Install Visual Studio 2022 Preview So, the first thing you have to do is download the [Visual Studio 2022 Preview installer](https://visualstudio.microsoft.com/vs/preview/#download-preview) from the Microsoft website. Then, click on **Mobile development with .NET** and verify if **.NET MAUI (Preview)** is checked. ![Visual Studio 2022 Preview MAUI - Start with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/img_6268092d3b3c4.png?resize=640%2C358&ssl=1)Visual Studio 2022 Preview MAUI Now, sit back and relax waiting for the installation. So, shall we start with MAUI? ## Create the first project Good! Now, open Visual Studio 2022 Preview and click on **Create a new project**. ![Create a new project with Visual Studio 2022 Preview - Start with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-19.png?resize=640%2C426&ssl=1)Create a new project with Visual Studio 2022 Preview Immediately at the top of the list, you have the new templates for **.NET MAUI**. I select the first one **.NET MAUI App (Preview)**. ![Create a new project - Start with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-22.png?resize=640%2C426&ssl=1)Create a new project Few seconds later, the application is ready! ![Visual Studio with the new MAUI project - Start with MAUI](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-23.png?resize=640%2C408&ssl=1)Visual Studio with the new MAUI project The first thing I see is that finally there is only one folder for the platforms. The code for Windows is finally here. In the first version of MAUI there was a [different project for Windows app](https://puresourcecode.com/dotnet/maui/install-maui-with-visual-studio-2022/). ![Solution Explorer](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-24.png?resize=452%2C1024&ssl=1)Solution Explorer Under **Platforms** folder, there is the specific code for each single platform. So, if you are going to build an Android application, you can see there is an `AndroidManifest.xml` for the specific configuration in Android. Those are just C# projection of the native platform classes. If you have a specific code for Android, you can add your class directly in that folder and the code will be compiled only for this specific platform. Then, the **Resources** folder. Here you collect images, fonts or raw files. Raw files are any raw assets you want to be deployed with your application and given a **Build Action** of `MauiAsset`. These files will be deployed with you package and will be accessible using Essentials: ``` async Task LoadMauiAsset() { using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); using var reader = new StreamReader(stream); var contents = reader.ReadToEnd(); } ``` One important thing to say is the namespace Microsoft.Maui.Essentials Updates doesn’t exists any more. It is replaced with more intuitive namespaces like `Microsoft.Maui.ApplicationModel`, `Microsoft.Maui.ApplicationModel.Communication` and so on. The full list is available on [GitHub](https://github.com/dotnet/maui/wiki/Migrating-to-RC1). Under **Resources**, you can also create a `Styles` folder to organize the styles for your application. ### Edit project Now, if you want to inspect or edit the project file, double-click on the project name in the **Solution Explorer**. First this I noticed is the `Target Frameworks`. Those are all the framework the application supports. ``` net6.0-android;net6.0-ios;net6.0-maccatalyst ``` If you strip out, for example, `net6.0-ios` the project for iOS won’t be compiled. Then, the `TargetFrameworks Condition` for Windows. If someone is talking about TFM (Target Framework Monitors), they refer to this one. ``` true true ``` Those 2 lines tell that we want to use MAUI and the code is in a single project. Those bring the MAUI dependecies in my project and enable the single features in the project (like Fonts and Images). ### MauiProgram Now, all the application starts from the `App.xaml.cs` class but in `MauiProgram.cs` there is the common configuration and is very simple ``` public static class MauiProgram { public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder(); builder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); }); return builder.Build(); } } ``` The code in line 7, tells that we want to use MAUI and then the fonts we want to use across the application. **Categories:** .NET Core **Tags:** maui, preview, visualstudio-2022, xamarin --- ### [MAUI Release Candidate is here](https://puresourcecode.com/news/maui-release-candidate-is-here/) **Published:** April 26, 2022 **Author:** Enrico **Excerpt:** The exciting news in April for mobile app development is that the release of .NET MAUI Release Candidate (RC) is here. **Content:** The exciting news in April for mobile app development is that the release of .NET MAUI Release Candidate (RC) is here, after few [announcements](https://puresourcecode.com/dotnet/net-core/install-maui-with-visual-studio-2022-preview/). To get started, ensure you have installed Visual Studio 2022 Preview 17.2 Preview 3 and check .NET MAUI under ‘Mobile Development with .NET Workload’. As with all .NET release candidates, .NET MAUI is supported for production apps, so you can truly get publishing safely. ## What about Xamarin Apps? For anyone concerned about Xamarin applications, support will continue as expected until November 2023, so there is no immediate pressure to migrate. However, there is a handy [migration sheet](https://github.com/dotnet/maui/wiki/Migrating-to-RC1) available on GitHub with tips to move from Xamarin to MAUI RC1 for those who want to get started. ## What’s in the MAUI Release Candidate? There are an astonishing 204 [changes listed on GitHub for RC1](https://github.com/dotnet/maui/releases/tag/6.0.300-rc.1) which we will not go through here. But in a brief recap of what is available to you in .NET MAUI, we have: - Platform SDKs for Android, iOS, macOS and Windows available to use directly with C# - Over 40 layouts and controls - Ability to incorporate Blazor components or full Blazor applications - A default stylesheet - Customized controls – low-code hooks to modify anything - And of course, pages, views, animation, brushes, pop-ups, graphics, shadows, styling, theming and visual states ## Where to start with MAUI? The team at Microsoft has released a .NET MAUI workshop which takes you through the process of building an app and is available here: [GitHub – dotnet-presentations/dotnet-maui-workshop: A full day workshop](https://github.com/dotnet-presentations/dotnet-maui-workshop). The complete documentation is also live, so you can get all your questions answered here: [.NET MAUI | Microsoft Docs](https://docs.microsoft.com/en-gb/dotnet/maui/get-started/first-app?pivots=devices-android). ## How to install MAUI? So, if you want to install MAUI, you have to install Visual Studio 2022 Preview. Then, open the Visual Studio Installer. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-17.png?resize=640%2C360&ssl=1)Visual Studio Installer In my laptop, you see I have 3 versions of Visual Studio. One is **Visual Studio Community 2022 Preview**. Click on the button **Modify** and be sure **.NET MAUI (Preview)** is checked. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-18.png?resize=640%2C358&ssl=1) **Categories:** MAUI, Microsoft, News **Tags:** maui, visual-studio --- ### [Add Security Headers to Blazor WebAssembly](https://puresourcecode.com/dotnet/net-core/add-security-headers-to-blazor-webassembly/) **Published:** April 5, 2022 **Author:** Enrico **Excerpt:** In this new post, I like to explain how to add security headers to Blazor WebAssembly to follow the recommendations from OWASP. **Content:** In this new post, I like to explain how to add security headers to [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) to follow the recommendations from [OWASP](https://owasp.org/). If you want to secure an ASP.NET Core Web Application, see my previous post “[Add Security Header Our Website](https://puresourcecode.com/dotnet/net-core/add-security-header-our-website/)“. The source code of this post is on [GitHub](https://github.com/erossini/ASPNETCoreOWASP). The demo website for this project is [here](https://observatory2.puresourcecode.com/). ## The issue When we create a new Blazor WebAssembly project, we assume the resulted website will be protected and with a minimum of security, also because Blazor is a new Microsoft technology. Unfortunately, it is not true. We discovered that there are a lot of open issues that we have to fix before deploying the project in production. ### How to discover the major issues? So, if you hire a penetration company they can suggest some expensive tools to use. Fortunately for us, there a couple of free tools we can use to test how website. #### OWASP Zap First, OWASP Zap is a tool build with Java that runs on your local machine and attaches your website to find vulnerability. This tool is open source and actively maintained by volunteers around the world. Now, you can download OWASP Zap from the [official website](https://www.zaproxy.org/). ![OWASP Zap website - Add Security Headers to Blazor WebAssembly](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/A4AC9C7E-19F4-443D-8F03-3FDBFD09D1AE.png?resize=640%2C480&ssl=1)OWASP Zap website Fron here, on the top right you see the button **Download**. From here, select the most appropriate version for your operating system and then run the installer. Probably, you need the Java Runtime. ![OWASP Zap Download page - Add Security Headers to Blazor WebAssembly](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/A7F81767-EB2A-4020-90E6-3E3B689522F4.png?resize=640%2C480&ssl=1)OWASP Zap Download page ### Observatory by Mozilla Then, another free tool you can use from a website. This is the [observatory by Mozilla](https://observatory.mozilla.org/). It is a good tool but unfortunately it is quite slow. You have a screenshot below. ![Observatory Mozilla website - Add Security Headers to Blazor WebAssembly](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/43F06273-139C-4125-9C70-C7B39780DA96.png?resize=640%2C480&ssl=1)Observatory Mozilla website First, you have to type or paste the URL to scan and the click on button **Scan Me**. After that, the website is waiting for a free instance to start the scan. Sometimes, you have to wait for ages. ### Security Headers [Security Headers](https://securityheaders.com/) analyse the HTTP response headers of other sites. Also, it adds a rating system to the results. The HTTP response headers that this site analyses provide huge levels of protection and it’s important that sites deploy them. Hopefully, by providing an easy mechanism to assess them, and further information on how to deploy missing headers, we can drive up the usage of security based headers across the web. ![Security Headers website - Add Security Headers to Blazor WebAssembly](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-12.png?resize=640%2C374&ssl=1)Security Headers website ## What is OWASP? The Open Web Application Security Project® ([OWASP](https://owasp.org/)) is a nonprofit foundation that works to improve the security of software. Through community-led open-source software projects, hundreds of local chapters worldwide, tens of thousands of members, and leading educational and training conferences, the OWASP Foundation is the source for developers and technologists to secure the web. - Tools and Resources - Community and Networking - Education & Training ## Create a new Blazor project So, I assume we start from scratch a new project. Then, open Visual Studio and select new **Blazor WebAssembly** project. Type the name you want and the folder. Then, you arrive in the **Additional Information** page. Because it is not possible to add middleware in the Blazor project, we have to use **ASP.NET Core hosted** for it also called **Blazor Server hosting model** (see [Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-6.0)). ![Create a new Blazor project - Additional information page - Add Security Headers to Blazor WebAssembly](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-8.png?resize=640%2C426&ssl=1)Create a new Blazor project – Additional information page With the Blazor Server hosting model, the app is executed on the server from within an ASP.NET Core app. UI updates, event handling, and JavaScript calls are handled over a [SignalR](https://docs.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-6.0) connection. The state on the server associated with each connected client is called a *circuit*. A circuit can tolerate temporary network interruptions and attempts by the client to reconnect to the server when the connection is lost. In this solution, we have 3 project: - Client - Server - Shared ![Solution structure](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-15.png?resize=534%2C947&ssl=1)Solution structure The **Client** project is the Blazor WebAssembly application. The **Server** is the ASP.NET Core web application that references the Client project. Then, the **Shared** project. Now, I deploy the Server project to my live server and I want to run the scan test. I use the Mozilla Observatory and after few second the result is **F** and the score is 20/100. So, it is quite bad. ![First scan of the Blazor WebAssembly project - Add Security Headers to Blazor WebAssembly](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-13.png?resize=640%2C776&ssl=1)First scan of the Blazor WebAssembly project Unfortunately, using Blazor we can’t have the perfect score. The maximum we can achieve will be a **B+** that it is an improvement from the **F** at the beginning. ### Why can’t we get the perfect score? First, we start with **Content Policy**. This one is the one that you cannot remove from Blazor because there is some inline scripts that run for Blazor. So, the best we can do for that is -20. **Cookies**: we don’t have any cookies on this project. The **Cross-origin Resource Sharing** is set by your hosting like [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/) is your deployment is on this cloud. Generally speaking, it should be ok. You might want to add to your code some settings to be more specific. Now, we can add the **HTTP Strict Transport Security** setting but the default values are not enough. We can add `app.UseHsts();` but we have to change the `MaxAge` at least. If your application is hosted in Azure, it doesn’t allow you to change the time. It is possible to fix it with **Azure Front Door**. Then, **Subresource Integrity** won’t be use and this is used when you have scripts pulling from another part. For example, if you want to add Stripe, you have to add in this configuration Stripe and the put the actual ash of the soap resource that you are pulling to make sure that is the right resource and it asn’t been changed. Finally, we have `X-Content-Type-Options`, `X-Frame-Options` and `X-XSS-Protection` that we are going to fix or at least improve the score. ## Implementation Now, the changes are mostly in the `Program.cs` file. If you host your web application on Azure, it is also better if you add a new `web.config`. **All the changes are in the Server project!** ### Add a new web.config So, this task is quite easy. In the Server project, add a new `web.config` and add this content ``` ``` This configuration will remove the Powered By [IIS](https://puresourcecode.com/dotnet/iis/asp-net-core-on-windows-with-iis/) or ASP.NET from the headers. ### Change Program.cs Now, the most important part. Here, the full source code of the `Program.cs` ``` var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); builder.Services.AddHsts(options => { options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(181); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. See https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.Use((context, next) => { context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue() { MustRevalidate = true, NoCache = true, NoStore = true, }; string mainUrl = "https://observatory2.puresourcecode.com/"; #if DEBUG mainUrl = "https://localhost:7063"; #endif context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("Content-Security-Policy", $"default-src 'self' {mainUrl} 'unsafe-inline' 'unsafe-eval'; " + $"script-src 'unsafe-inline' 'unsafe-eval' {mainUrl}; " + "connect-src 'self'; " + $"img-src 'self' {mainUrl}; " + $"style-src 'self' {mainUrl}; " + "base-uri 'self'; " + "form-action 'self'; " + "frame-ancestors 'none';"); context.Response.Headers.Add("Referrer-Policy", "same-origin"); context.Response.Headers.Add("Permissions-Policy", "geolocation=(), microphone=()"); context.Response.Headers.Add("X-XSS-Protection", "1; mode=block"); context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); context.Response.Headers.Add("SameSite", "Strict"); return next.Invoke(); }); app.UseHttpsRedirection(); app.UseHsts(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.MapRazorPages(); app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run(); ``` So, I added to the boilerplain file only few new lines. Here the explanation: - 6-11: update the values for HTTP Strict Transport Security (HSTS) header set to a minimum of *six months* - 27: add the middleware. So, for each request the middleware will add this headers - 29: add cache control - 37: add a variable for the main URL. It changes if the application is in debug. So, we won’t have local addresses in production - 42-56: add the security headers - 61: force to redirect the requests to HTTPS - 62: use HSTS This is all the code we need to improve the security in our Blazor WebAssembly application. Please let me know if I can improve more, leaving your comment below or in the [forum](https://puresourcecode.com/forum/). Now, we have to check our hard work. ## The result After deploying the demo application on a live server, I run Mozilla Observatory and after few seconds… the result. It is what I expected from the beginning: a stunning **B+** and a score of **80/100**! As I said at the beginning, we can’t improve the **Content Security Policy**. ![Mozilla Observatory website with the result](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-14.png?resize=640%2C806&ssl=1)Mozilla Observatory website with the result So, next stop Security Headers. Again, I type the address and here the result: **A**! This is not bad at all. ![Security Headers with the result](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-9.png?resize=640%2C583&ssl=1)Security Headers with the result My last step is to use OWASP Zap to check if there are more vulnerabilities. After launching the application, I type in the **URL to attack**, the address where the application lives and the click on **Attach**. The result is in line with what I expect. ![OWASP Zap with the result](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-16.png?resize=640%2C473&ssl=1)OWASP Zap with the result I’m not sure if the string, that is similar to a timestamp in UNIX, in the CSS is really a threat or not. The CSP is what we can’t fix for Blazor. ## Random Blazor “Failed to find a valid digest in the ‘integrity’ attribute for resource” This is an issue when loading a site built and published using Blazor. I get the following error message: > Failed to find a valid digest in the ‘integrity’ attribute for resource ‘https://MYWEBSITEURL.com/\_framework/System.Private.CoreLib.dll’ with computed SHA-256 integrity ‘xV9SflNt5Ex5gP7OznQorlp2VkdJXkcAiopU+h5DRzY=’. The resource has been blocked. I assume that the browser blocks the files from downloading because the hashes created when publishing do not match. To fix this error, we have to change the `index.html` file in the Client project. It is necessary to replace the `script` related to `service-worker.js` with this code: ``` ``` **Categories:** .NET Core, .NET6, Blazor **Tags:** aspnet-5, aspnet-core, azure, blazor, blazor-server, blazor-webassembly, net6, owasp **Hashtags:** blazor, security --- ### [Add Security Header our website](https://puresourcecode.com/dotnet/net-core/add-security-header-our-website/) **Published:** April 4, 2022 **Author:** Enrico **Excerpt:** In this new post, I'll show you how to add security header to our website to be ready for a penetration test. Quick and easy to implement. **Content:** In this new post, I’ll show you how to add security header to our website to be ready for a penetration test. This is something that we don’t look at until it is too late and we receive the report from the penetration company. The source code of this project is on [GitHub](https://github.com/erossini/ASPNETCoreOWASP). The demo website for this project is [here](https://observatory.puresourcecode.com/). ## How can we test our website? First, there are a lot of tools we can use to have a first check of your website. I like to use [Observatory by Mozilla](https://observatory.mozilla.org/). This is a online tool that helps to show the major issues with some recommendations. ![Mozilla Observatory website - Add Security Header our website](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-3.png?resize=640%2C347&ssl=1)Mozilla Observatory website There is another useful website with the same functionality: [Security Headers](https://securityheaders.com/) checks your website and gives you some more information, in particular, about new headers. ![Security Header home page - Add Security Header our website](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-5.png?resize=640%2C459&ssl=1)Security Header home page In both cases, you have to type in the address only the domain without `http` or `https`. ### Create a test website Now, open your [Visual Studio](https://puresourcecode.com/category/tools/visual-studio-tools/) and you create an **ASP.NET Core Web**. ![Create an ASP.NET Core Web App](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-4.png?resize=640%2C426&ssl=1)Create an ASP.NET Core Web App First, I deploy this basic project without any change on my online server. Now, I go to the Observatory from Mozilla and I run the test. After few seconds, I have this result and this is the screenshot. ![ASP.NET Core website out of the box - Add Security Header our website](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-1.png?resize=640%2C772&ssl=1)ASP.NET Core website out of the box So, I think it is not great. For this reason, I have to add some security header to our website to improve its quality. Let’s start. ## Security Headers Here you have the list of the most important security headers and a brief explanation. For more information about each header, there is a link to the documentation. ### Strict-Transport-Security HTTP Strict Transport Security (HSTS) protect websites against man-in-the-middle attacks by indicating the browser to access the website using HTTPS instead of using HTTP. More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security). ### X-Frame-Options The `X-Frame-Options` HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a ``, ``, `` or ``. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites. More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). ### X-Permitted-Cross-Domain-Policies The `X-Permitted-Cross-Domain-Policies` HTTP response header can be used to indicate whether or not an Adobe products such as Adobe Reader should be allowed to render a page. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other applications. More info on the [Adobe website](https://www.adobe.com/devnet-docs/acrobatetk/tools/AppSec/xdomain.html). ### X-XSS-Protection The HTTP `X-XSS-Protection` response header is a feature that stops pages from loading when they detect reflected cross-site scripting (XSS) attacks. Although these protections are largely unnecessary in modern browsers when sites implement a strong `Content-Security-Policy` that disables the use of inline JavaScript (`'unsafe-inline'`), they can still provide protections for users of older web browsers that don’t yet support CSP. More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection). ### X-Content-Type-Options The `X-Content-Type-Options` response header is a marker used by the server to indicate that the MIME types advertised in the `Content-Type` headers should not be changed and be followed. This is a way to opt-out of MIME type sniffing, or, in other words, to say that the MIME types are deliberately configured. More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options). ### Referrer-Policy The `Referrer-Policy` header controls how much referrer information (sent via the `Referer` header) should be included with requests. This may prevent information disclosure as URLs may contain sensitive data. More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy). ### Feature-Policy The `Feature-Policy` header provides a mechanism to allow and deny the use of browser features in its own frame, and in content within any `` elements in the document. It can prevent the use of sensible APIs such as microphone, or it can help to fix performance issues such as using oversized images. More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy). ### Expect-CT The `Expect-CT` header lets sites opt-in to reporting and/or enforcement of [Certificate Transparency](https://developer.mozilla.org/en-US/docs/Web/Security/Certificate_Transparency) requirements, to prevent the use of mis-issued certificates for that site from going unnoticed. This helps detecting man-in-the-middle attacks by someone that could generate a certificate for your domain. Cloudflare has a service to monitor certificate generation: [Introducing Certificate Transparency Monitoring](https://blog.cloudflare.com/introducing-certificate-transparency-monitoring/). More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT). ### Content-Security-Policy The `Content-Security-Policy` response header allows web site administrators to control resources the user agent is allowed to load for a given page. With a few exceptions, policies mostly involve specifying server origins and script endpoints. This helps guard against cross-site scripting attacks (XSS). More info on the [Mozilla website](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy). ## The implementation So, the project I created is build with [NET6](https://puresourcecode.com/tag/net6/). That means, the configuration is in the `Program.cs` and there is not start up file. Also, I like to remove the header `X-Powered-By` introduced by Microsoft for [IIS](https://puresourcecode.com/category/dotnet/iis/). If this header is present, the website has a low rating. ### Add web.config First, this is easy. We have to add to our project a `web.config` and add this XML: ``` ``` So, this configuration will remove the header. That’s it. ### Update Program.cs Now, I think it is more simple if I show you the entire code and then comment it. ``` var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, // see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.Use((context, next) => { context.Response.GetTypedHeaders().CacheControl = new Microsoft.Net.Http.Headers.CacheControlHeaderValue() { MustRevalidate = true, NoCache = true, NoStore = true, }; context.Response.Headers.Add("X-Content-Type-Options", "nosniff"); context.Response.Headers.Add("Content-Security-Policy", "default-src 'none'; " + "script-src 'self'; " + "connect-src 'self'; " + "img-src 'self'; " + "style-src 'self'; " + "base-uri 'self'; " + "form-action 'self'; " + "frame-ancestors 'none';"); context.Response.Headers.Add("Referrer-Policy", "strict-origin"); context.Response.Headers.Add("Permissions-Policy", "geolocation=(), microphone=()"); return next.Invoke(); }); app.UseHsts(); app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); ``` Now, we can see there are only few lines more than the `Program.cs` generated by Visual Studio. I highlighted the new lines. First, line 17 starts to create a middleware that it will be called before each request. Then, I set the `Cache-Control` (line 20). Now, the interesting part between lines 27 and 37; I add the headers. In this example all files (images, fonts, style sheets…) are coming from the website domain. Then, I use the function that ASP.NET Core provides for HTTPS redirection and HSTS (lines 42-43). For a better score, we can change the HSTS because the default can create some issues. We can use this code after line 4: ``` builder.Services.AddHsts(options => { options.Preload = true; options.IncludeSubDomains = true; options.MaxAge = TimeSpan.FromDays(60); options.ExcludedHosts.Add("example.com"); options.ExcludedHosts.Add("www.example.com"); }); ``` ## The result So, we added security header to our website to obtain a good score. What is the result? First, I run again the **Observatory by Mozilla** and after few seconds the result is really good. The website has **A+**. I want to point out the score **120/100**!!! ![The result from Mozilla Observatory](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-7.png?resize=640%2C770&ssl=1)The result from Mozilla Observatory Now, it is time to test the website also with Security Headers website. So, after few seconds, the result is still stunning: **A+**. ![Scan with Security Headers](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/04/image-6.png?resize=640%2C610&ssl=1)Scan with Security Headers So, this is a very good job! Well done! ## Wrap up In conclusion, in this post **Add Security Header our website**, we learn how to improve the security of our website adding specific headers. So, browsers can render the pages in a more secure way than before and the communication is more protected. **Categories:** .NET Core, .NET5, .NET6, ASP.NET **Tags:** aspnet, aspnet-5, aspnet-core, net6, netcore **Hashtags:** aspnet-core, security --- ### [Query in Azure DevOps for work items](https://puresourcecode.com/tools/azure-devops/query-in-azure-devops-for-work-items/) **Published:** March 8, 2022 **Author:** Enrico **Excerpt:** It is possible to write custom query in Azure DevOps for work items using the Work Item Query Language (WIQL) a language close to SQL language **Content:** Having [processes](https://puresourcecode.com/tools/azure-devops/azure-devops-processes/), it ends up in request of custom query in Azure DevOps for work items. I have discovered that it is possible to write specific queries using a kind of SQL. Its name is **WIQL**. A query defined using the **Work Item Query Language** (WIQL) consists of a `SELECT` statement that lists the fields to be returned as columns in the result set. So, you can further qualify the result set by using a logical expression. You can specify a sort order. Use an `ASOF` clause to state that a query was evaluated previously. ## Install the editor Now, from your organization, you can search in the [Marketplace for the Wiql Editor](https://marketplace.visualstudio.com/items?itemName=ottostreifel.wiql-editor) or click on the link that brings you to the following page. ![WIQL Editor on Microsoft Marketplace - Query in Azure DevOps for work items](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image-5.png?resize=640%2C446&ssl=1)WIQL Editor on Microsoft Marketplace Then, when the WIQL Editor is installed, you can find it under **Boards** in your project pages. ![WIQL Editor in the Boards](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image-6.png?resize=640%2C403&ssl=1)WIQL Editor in the Boards ## Work Item Query Language So, the work item query language has five parts shown in the following syntax snippet and described in the following table. ``` SELECT [State], [Title] FROM WorkItems WHERE [Work Item Type] = 'User Story' ORDER BY [State] Asc, [Changed Date] Desc ASOF '6/15/2010' ``` The WIQL syntax isn’t case-sensitive. ### Limits on WIQL length The WIQL length of queries made against Azure Boards must not exceed 32K characters. The system won’t allow you to create or run queries that exceed that length. ClauseExampleSELECT Identifies the fields to return for each work item returned by the query. You can specify either the friendly name or reference name. Use square brackets (\[\]) if the name contains blanks or periods.FROMIndicates whether you want the query to find work items or links between work items. Use `FROM WorkItems` to return work items. Use `FROM workItemLinks` to return links between work items. For more information, see [Queries for links between work items](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops#linked-work-items) later in this article.WHERESpecifies the filter criteria for the query. For more information, see [Filter conditions (WHERE)](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops#where-clause) in the next section.ORDER BYSpecifies the sort order of the work items returned. You can specify Ascending (Asc) or Descending (Desc) for one or more fields. For example: `ORDER BY [State] Asc, [Changed Date] Desc`ASOFSpecifies a historical query by indicating a date or when the filter is to be applied. For example, this query returns all user stories that existed on June 15, 2019. `ASOF '6/15/2019'`## WHERE filter conditions The `WHERE` clause specifies the filter criteria. The query returns only work items that satisfy the specified criteria. For example, the following example `WHERE` clause returns user stories that are active and that are assigned to you.WIQLCopy ``` WHERE [Work Item Type] = 'User Story' AND [State] = 'Active' AND [Assigned to] = @Me ``` You can control the order in which logical operators are evaluated by enclosing them within parentheses to group the filter criteria. For example, to return work items that are either assigned to you or that you closed, change the query filter to match the following example.WIQLCopy ``` WHERE [Work Item Type] = 'User Story' AND [State] = 'Active' AND ( [Assigned to] = @Me OR [Closed by] = @Me ) ``` ### [](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops#filter-conditions)Filter conditions Each filter condition is composed of three parts, each of which must conform to the following rules: - **Field**: You can specify either the reference name or friendly name. The following examples are valid WIQL syntax: - Reference name with spaces: `SELECT [System.AssignedTo] ...` - Friendly name with spaces: `SELECT [Assigned To] ...` - Names without spaces don’t require square brackets: `SELECT ID, Title ...` - **Comparison operator**: Valid values are specified in the [Operators](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops#operators) section later in this article. – - **Field value**: You can specify one of the following three values depending on the field specified. - A *literal value* must match the data type of the field value. - A \*variable or macro that indicates a certain value. For example, @Me indicates the person who is running the query. For more information, see [Macros and variables](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops#macros) later in this article. - The name of another *field*. For example, you can use `[Assigned to] = [Changed by]` to find work items that are assigned to the person who changed the work item most recently. For a description and reference names of all system-defined fields, see [Work item field index](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/work-item-field?view=azure-devops). ### [](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops#operators)Operators Queries use logical expressions to qualify result sets. These logical expressions are formed by one or more conjoined operations. Some simple query operations are listed below. ``` WHERE [System.AssignedTo] = 'joselugo' WHERE [Adatum.CustomMethodology.Severity] >= 2 ``` The table below summarizes all the supported operators for different field types. For more information on each field type, see [Work item fields and attributes](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/work-item-fields?view=azure-devops). The `=, , >, =, and 100` queries for all work items with an **ID** greater than 100. `System.ChangedDate > '1/1/19 12:00:00'` queries for all work items changed after noon of January 1, 2019. Beyond these basic operators, there are some behaviors and operators specific to certain field types. Field typeSupported operatorsBoolean= , <> , =\[Field\] , <>\[Field\]DateTime= , <> , > , < , >= , <= , =\[Field\], <>\[Field\], >\[Field\], <\[Field\], >=\[Field\], <=\[Field\], In, Not In, Was EverDouble, GUID, Integer= , <> , > , < , >= , <= , =\[Field\], <>\[Field\], >\[Field\], <\[Field\], >=\[Field\], <=\[Field\], In, Not In, Was EverIdentity= , <> , > , < , >= , <= , =\[Field\], <>\[Field\], >\[Field\], <\[Field\], >=\[Field\], <=\[Field\], Contains, Does Not Contain, In, Not In, In Group, Not In Group, Was EverPlainTextContains Words, Does Not Contain Words, Is Empty, Is Not EmptyString= , <> , > , < , >= , <= , =\[Field\], <>\[Field\], >\[Field\], <\[Field\], >=\[Field\], <=\[Field\], Contains, Does Not Contain, In, Not In, In Group, Not In Group, Was EverTreePath=, <>, In, Not In, Under, Not Under### Logical groupings So, you can use the terms `AND` and `OR` in the typical Boolean sense to evaluate two clauses. You can use the terms `AND EVER` and `OR EVER` when specifying a WAS EVER operator. Now, you can group logical expressions and further conjoin them, as needed. Examples are shown below. ``` WHERE [System.State] = 'Active' AND [System.AssignedTo] = 'joselugo' AND ([System.CreatedBy] = 'linaabola' OR [Adatum.CustomMethodology.ResolvedBy] = 'jeffhay') AND [System.State] = 'Closed' WHERE [System.State] = 'Active' AND [System.State] EVER 'Closed' ``` You can negate the `contains, under,` and `in` operators by using `not`. You can’t negate the `ever` operator. The examples below query for all work items that aren’t classified within the subtree of ‘MyProject\\Feature1’. ``` WHERE [System.AreaPath] not under 'MyProject\Feature1' WHERE [System.AssignedTo] ever 'joselugo' ``` For more documentation, visit the [Microsoft documentation](https://docs.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops). ## Querying all PBIs in an Epic ``` SELECT [System.Id], [System.WorkItemType], [System.Title], [System.AssignedTo], [System.State], [System.Tags] FROM workitemLinks WHERE ( [Source].[System.TeamProject] = @project AND [Source].[System.WorkItemType] = 'Epic' AND [Source].[System.State] '' AND [Source].[System.Id] = {Epic ID} ) AND ( [System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward' ) AND ( [Target].[System.TeamProject] = @project AND [Target].[System.WorkItemType] = 'Product Backlog Item' ) ORDER BY [System.Id] MODE (Recursive) ``` ![The query in the editor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/RTde8.png?w=640&ssl=1)The query in the editor As a workaround, when we add child item(PBI) to an epic, we can add a same tag and get the result via query tag, then we can create a dashboard chart to see the number and states of the PBIs in a given Epic. ![The result as a list and as chart](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/fpVRm.png?w=640&ssl=1)The result as a list and as chart **Categories:** Azure DevOps **Tags:** agile, azure-devops, processes, wiql, work-items --- ### [Validate JSON with Postman](https://puresourcecode.com/tools/validate-json-with-postman/) **Published:** March 8, 2022 **Author:** Enrico **Excerpt:** In this new post, I explain how to validate a JSON result with Postman from your APIs using JavaScript with few lines of code. Full code here **Content:** In this new post, I explain how to validate a JSON result with Postman from your APIs. In [Testing APIs with RestClient in Visual Studio Code](https://puresourcecode.com/dotnet/net-core/testing-apis-with-restclient-in-visual-studio-code/) post, I talk how to use the [RestClient](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) to test APIs. Also, I started to use another extension for Visual Studio Code that it is very simple to use. [Thunder Client](https://www.thunderclient.com/) is the best extension to test APIs in a very similar design of Postman. After few consideration, I decided to go back to Postman instead of using other tools. In particular because it has the integration with JavaScript that allows me to write some custom tests. ## What is Postman? So, [Postman](https://www.postman.com/) is a useful tool to test API requests. Doesn’t matter if we are developing our own APIs or we are using a third party provider APIs. We can check all the data received on each request. Then, I will explain how to validate JSON with Postman writing tests along with each request. Also, I show how to check if the calls are successful and if json data received meets the expected JSON schema. ![Postman first time - Validate JSON with Postman](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image.png?resize=640%2C457&ssl=1)Postman first time ## Check request result The first and simpler test that we can include is to validate the request has succeded, and we got the ***Response 200 OK***, or any other response code we may be expecting. To do so, the easiest way is to include the test at the Collection level, so that all the requests underneath will inherit this validation. So you can edit the Collection, select the “**Tests**” tab, and include the following script: ``` pm.test("Status code is OK", function () { pm.expect(pm.response.code).to.be.oneOf([200, 201, 204, 207]) }); ``` You can include any other response codes you might be expecting from your requests as well. Once set up, on any request execution, you should see the test result in the response “**Tests**” tab: ![Test the response code in Postman - Validate JSON with Postman](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image-1.png?resize=640%2C352&ssl=1)Test the response code in Postman ## Response JSON schema validation Now, Postman scripting is quite a powerful asset, so you can go beyond a simple response code validation and include more complex validations, like checking that the response JSON meets a determined schema. ### What is a JSON schema? JSON Schema is a grammar language for defining the structure, content, and (to some extent) semantics of JSON objects. It lets you specify metadata (data about data) about what an object’s properties mean and what values are valid for those properties. The result of applying the grammar language to a JSON document is the schema (a blueprint) describing the set of JSON objects that are valid according to the schema. 1. JSON Schema is itself a JSON object. 2. JSON Schema grammar is maintained at [https://json-schema.org](https://json-schema.org/). 3. It describes the existing data format. 4. If offers clear, human-readable, and machine-readable documentation. 5. It provides complete structural validation, which is useful for automated testing and validating client-submitted data. ### Create a JSON schema for a JSON So, the first step is to create the JSON schema from a JSON. For example, when we design an API, we decide what the inputs/parameters are what the result JSON looks like. For example, I have one API that returns a connection token and it looks like ``` { "access_token": "eyJhbGciOiJS...", "expires_in": 3600, "token_type": "Bearer", "scope": "email openid profile roles smp" } ``` Now, from this JSON I want to create the JSON schema. There are a lot of tools online and offline. I found quite straightforward and easy to use [jsonformatter.org](https://jsonformatter.org). There is a page that creates a JSON schema from a JSON and this tool is on [this page](https://jsonformatter.org/json-to-jsonschema). Now, I copy the JSON and paste on the left side and immediately, I have the correspondent JSON schema. ``` { "$schema": "https://json-schema.org/draft-06/schema#", "$ref": "#/definitions/Welcome2", "definitions": { "Welcome2": { "type": "object", "additionalProperties": false, "properties": { "access_token": { "type": "string" }, "expires_in": { "type": "integer" }, "token_type": { "type": "string" }, "scope": { "type": "string" } }, "required": [ "access_token", "expires_in", "scope", "token_type" ], "title": "Welcome2" } } } ``` Here the screenshot. ![jsonformatter.org in action](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image-2.png?resize=640%2C251&ssl=1)jsonformatter.org in action Now, we have our JSON schema we can add the code in Postman. ### Add the schema in Postman Once you got the schema, back to Postman, select the request you want to be validated, and edit the “**Pre-request Script**” tab, where you should add the following script (*using the schema from the step before*): ![Add script in Pre-request Script in Postman](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image-3.png?resize=640%2C256&ssl=1)Add script in Pre-request Script in Postman How you can see in the screenshot, I define a variable `schema` and then assign the JSON schema. At the end, a semicolon `;` is required. This is JavaScript. You have the full code here ``` var schema = { "$schema": "https://json-schema.org/draft-06/schema#", "$ref": "#/definitions/Welcome2", "definitions": { "Welcome2": { "type": "object", "additionalProperties": false, "properties": { "access_token": { "type": "string" }, "expires_in": { "type": "integer" }, "token_type": { "type": "string" }, "scope": { "type": "string" } }, "required": [ "access_token", "expires_in", "scope", "token_type" ], "title": "Welcome2" } } }; ``` Now, we have to tell Postman that we want to use this variable in the request. After the definition of the `schema` variable, add this code ``` pm.variables.set("schema", schema); ``` So, Postman (`pm`) sets a new variable `schema` that we can use in the test. ### Add the response JSON validation Postman scripting is quite a powerful asset, so you can go beyond a simple response code validation and include more complex validations, like checking that the response JSON meets a determined schema. In order to execute such validation, back to the “**Tests**” tab in the Collection to include the following script, right after the previous script we added in the previous step: ``` try { var schema = pm.variables.get("schema"); if(schema) { const jsonData = pm.response.json(); if(jsonData) { var Ajv = require('ajv'); ajv = new Ajv({ logger: console, allErrors: true }); pm.test('Schema is valid', function() { var result = tv4.validateMultiple(jsonData, schema); console.log(result); pm.expect(result.valid).to.be.true; }); } } } catch(e) { console.log(e); } ``` #### Code explained First, I read the schema from the variables. Then, if it is not null, I parse and save the API response in the `jsonData`. Then, I use [AJV](#ajv) and log all the errors. Now, I ask Postman `pm` to run a test. Then, I use [tv4](#tv4) to validate the JSON data with the schema. If the result is valid, the test is passed. ![Test results in Postman](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/03/image-4.png?resize=640%2C420&ssl=1)Test results in Postman ### Ajv JSON schema validator [Ajv JSON schema validator](https://ajv.js.org/) is used by a large number of JavaScript applications and libraries in all JavaScript environments – Node.js, browser, Electron apps, WeChat mini-apps etc. It allows implementing complex data validation logic via declarative schemas for your JSON data, without writing code. #### TV4 TV4 is another tools that Postman is using. [Tiny Validator (for v4 JSON Schema)](https://github.com/geraintluff/tv4) is another JavaScript library to validate simple values and complex objects using a rich [validation vocabulary](https://json-schema.org/latest/json-schema-validation.html). ## Wrap up How you see, in few line of code, there are a lot of functionalities and concepts to know. I hope this post helps you to write better tests for your API. Please leave your comments and thoughts in the [forum](https://puresourcecode.com/forum/). **Categories:** Tools **Tags:** api, json, json-schema, postman, webapi **Hashtags:** api, json, json-schema, postman, webapi --- ### [Handling file uploads in OpenAPI](https://puresourcecode.com/dotnet/net-core/handling-file-uploads-in-openapi/) **Published:** March 8, 2022 **Author:** Enrico **Excerpt:** This post is about implementing handling file uploads in OpenAPI with ASP.NET Core. Open API is one way to document REST API endpoints. **Content:** This post is about implementing handling file uploads in [OpenAPI](https://puresourcecode.com/?s=open%20api) with [ASP.NET Core](https://puresourcecode.com/tag/aspnet-core/). Open API is one way to document [REST API](https://puresourcecode.com/?s=rest) endpoints. When we using Web API and `IFormFile` class to upload a file, [OpenAPI](https://docs.microsoft.com/en-us/aspnet/core/web-api/Microsoft.dotnet-openapi?view=aspnetcore-6.0) will display a File Upload control in the UI like this. Also, I recommend to read my other post [Uploading files in ASPNET Core](https://puresourcecode.com/dotnet/net-core/uploading-files-in-aspnet-core/) if you have to upload big file. ## Minimal API So, with NET6, Microsoft introduced [minimal APIs](https://puresourcecode.com/dotnet/net6/minimal-apis-in-net6/): the core idea behind minimal APIs is to remove some of the ceremony of creating simple APIs. It means defining lambda expressions for individual API calls. Now, this is an example for an `upload` method to upload a file via the API with Swagger. ``` app.MapPost("/upload", async (IFormFile file) => { //Do something with the file return Results.Ok(); }).Accepts("multipart/form-data").Produces(200); ``` Which will render something like this. ![File Upload in Open API](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/file_upload_openapi.png?w=640&ssl=1) Earlier versions of Open API won’t render it properly. And if we are using multiple `IFormFile` elements this won’t work properly. Here is an example. ``` app.MapPost("/upload-multiple", async (IFormFile[] files) => { //Do something with the files return Results.Ok(); }).Accepts("multipart/form-data").Produces(200); ``` Which will result something like this. ![Multiple File Upload in Open API](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/files_upload_openapi.png?w=640&ssl=1) And if we not using `IFormFile` and using `Request.Form` object to receive file upload then also it will not render properly. We can fix this by introducing a custom `OperationFilter` implementation. ## The implementation So, here is the code to manage multiple files. ``` public class FileUploadOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var fileUploadMime = "multipart/form-data"; if (operation.RequestBody == null || !operation.RequestBody.Content.Any(x => x.Key.Equals(fileUploadMime, StringComparison.InvariantCultureIgnoreCase))) { return; } var name = context.ApiDescription.ActionDescriptor.DisplayName; operation.Parameters.Clear(); if (context.ApiDescription.ParameterDescriptions[0].Type != typeof(IFormFile)) { var uploadFileMediaType = new OpenApiMediaType() { Schema = new OpenApiSchema() { Type = "object", Properties = { ["files"] = new OpenApiSchema() { Type = "array", Items = new OpenApiSchema() { Type = "string", Format = "binary" } } }, Required = new HashSet() { "files" } } }; operation.RequestBody = new OpenApiRequestBody { Content = { ["multipart/form-data"] = uploadFileMediaType } }; } } } ``` And we can include this in the UI like this. ``` builder.Services.AddSwaggerGen(setup => { setup.OperationFilter(); } ``` Which will render something like this – clicking on `Add string item` button will add File Upload controls. ![Multiple File Upload in Open API](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/multiple_file_upload_openapi.png?w=640&ssl=1)Post request in Swagger We can modify the `FileUploadOperationFilter` code and use the same code if you’re using `Request.Form` to accept files in the server side. ## Wrap up In conclusion, this is how handling file uploads in OpenAPI. Please leave your comment in our [forum](https://puresourcecode.com/forum/). **Categories:** .NET Core, ASP.NET **Tags:** api, net6, swagger, swagger-ui, webapi --- ### [Azure DevOps Processes](https://puresourcecode.com/tools/azure-devops-processes/) **Published:** February 25, 2022 **Author:** Enrico **Excerpt:** Azure DevOps provides 4 processes as standard. When creating a project, the default process is Agile. In details all Azure DevOps processes **Content:** [Azure DevOps](https://puresourcecode.com/tag/azure-devops/) provides 4 processes as standard. When creating a project, the default process is Agile. However, by browsing to Advanced, you will be prompted to choose between the available processes. Basic: This is the simplest process provided by Azure DevOps. The basic process tracks tasks from not started, doing and done and only tracks three types of work items. These are epics, issues and tasks. In part two of this blog, we will review the differences between these work items. **CMMI**: The Capability Maturity Model index closely relates to a standard waterfall methodology. It includes work items which allows a formal change management process. Work items which allows the project team access to Risks, Issues and Decisions from within Azure DevOps. **Agile**: The agile process is, as you’d expect it to be, a process which adheres to Agile principles. So, the terminology used to define work items as well as the functionality available makes this process quite popular for projects running an agile methodology. **Scrum**: Scrum is a variation of agile with different terminology. Azure DevOps, similarly with the agile process has terminology specific to the Scrum process. You may question having both Agile and Scrum processes and be confused about the differences. One of the main differences that you’ll encounter is that within the Agile process, tasks can be tracked by there original estimate, remaining work as well as completed work. However, if using the scrum process, tasks can only be tracked by the task’s remaining work. ## Overview of the Basic Process First, each standard process provides different work items which allows users to track work in different ways. ### Epics Now, epics are considered the top of the hierarchy within Azure DevOps. As such this work item really defines the process at a high level and what features this may include. ### Issues So, epics can have one or more associated issues. Issues, when using the basic process is not meant in the context of a problem. Instead it is to group at a high level what needs to be accomplished in order to achieve the epic. ### Task Now, a task is meant to be associated to Issues to define at a low level. A task needs to be done in order to close the defined issues. Also, each work item has a list of available attributes/fields. Interestingly enough, not all fields which are available are added to the work item form. Therefore, it’s definitely advisable to check that a field exists for what you need before creating another one. Here are the defined lists of fields available for the epic, issue and task work items: [Basic Process Available Fields](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/basic-field-reference?toc=%2Fazure%2Fdevops%2Fboards%2Ftoc.json&bc=%2Fazure%2Fdevops%2Fboards%2Fbreadcrumb%2Ftoc.json&view=azure-devops). ### Useful fields #### History Each work item has a discussion area to allow comments related to the work item. The discussion area contains timestamped records of each comment made by each user. The comments are saved in the history field and can be used in queries in Azure DevOps. ![Search for Work Items using History field - Azure DevOps Processes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-1.png?w=640&ssl=1)Search for Work Items using History field #### **Board Column and Board Column Done** A feature available within the Azure DevOps Kanban boards is the ability to split columns. This allows greater flexibility in team members. Specifying, the work has been completed or that work has begun in the next stage of the process. ![Split Column with Doing/Done - Azure DevOps Processes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-2.png?w=640&ssl=1)Split Column with Doing/Done Therefore, the Board Column attribute allows us to view work items within the specified board column. However, the Board Column Done attribute shows us work items which have been moved to the done column. This happens when split columns has been activated. ![Using Board Column Done in a query - Azure DevOps Processes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-3.png?w=640&ssl=1)Using Board Column Done in a query As each work item progresses, they go through different states. The states available by default differ based on the process selected. The basic process has only three states – To Do, Doing and Done. ![Basic State Workflow - Azure DevOps Processes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-4.png?w=640&ssl=1)Basic State Workflow Each state also has a reason which adds some more clarity about the state of the work item. For e.g a task that has just been added will default to a stage of *To Do* but will have a reason of *Added to Backlog*, when the Task is moved to *Doing*, the reason changes to *started*. If the task is moved back to a state of *to do*, the reason will not revert to *added to backlog* but will instead be *moved to the backlog*. The states and workflows, like the work item attributes and forms can be customised. This will be covered in a later blog where we detail how work items can be customised. ## Overview of the CMMI Process The CMMI ( Capability Maturity Model Index) process closely aligns to the waterfall methodology. The CMMI process within Azure DevOps is based on a guide published by the Software Engineering Institute called [CMMI for Development: Guidelines for Process Integration and Product Improvement (SEI Series in Software Engineering)](https://www.amazon.co.uk/CMMI-Development-Integration-Improvement-Engineering/dp/0321711505). The below image displays the typical stages and outputs within a standard waterfall project. As you read further into the blog, you will notice that the stages are similar to the states available within key work items in the CMMI process and that the terminology used to describe work items are also a close match to the standard waterfall methodology. ![Standard Waterfall Methodology Proces - Azure DevOps Processes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-6.png?w=640&ssl=1)Standard Waterfall Methodology Process The work items available within the CMMI process are shown in the image below and described further on: ![CMMI work items](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-7.png?w=640&ssl=1)CMMI work items ### Epics Now, epics within the CMMI process are the same as Epics within the Basic process. They contain the high level process of what is to be delivered. An example of this would be the delivery of different channels e.g phone or chat within a call centre. ### Features I explain how epics contain one or more features. Features allows the definition of different focus points which must be delivered in order for the epic to be delivered. E.g telephony integration and call verification features would be required in order to deliver an epic related to the delivery of a telephone channel in a call centre. ### Requirements Requirements describe the needs of the business and also defines what is required for the feature to be completed. They can be associated to one or more Features. It should be noted that a requirement is not a feature and should contain enough information for both testers and developers to understand the business need and what the system is expected to do to satisfy the need. Requirements which define how the system will satisfy the need reduces the flexibility of designing the system and may create issues later in the project as technically and strictly speaking, changing how the requirement is implemented would mean that a change request should be generated. ### Tasks Tasks provide a breakdown of what needs to be completed in order to complete a requirement. An example of a task which could be associated with the above requirement could be to create the credit limit field on the contact record. (Assuming you’re using a CDS database the name, address and date of birth fields should already exist). ### Change Requests Deviations from the agreed scope, should be logged as a change request. The CMMI process provides a work item which can be associated to the original requirement/s or feature/s being affected by the change and also allows the impact of the change to be documented. The below image shows an example of the change request work item. ![CMMI Change Request Work Item](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image.png?w=640&ssl=1)CMMI Change Request Work Item ### Review The review work item allows users to document meetings within Azure DevOps. These could be review meetings, design meetings or even team meetings. Attendees who are also Azure DevOps users can be selected and other work items which was discussed within the meeting associated for reference. This work item is only available by default with the CMMI process template. ### Issue The issue work item available within the CMMI process template relates to project related problems and is not the same issue work item used within the Basic process template. ![CMMI Issue Work Item](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/1_image-8.png?w=640&ssl=1)CMMI Issue Work Item The CMMI issue work item allows users to describe the issue as well as detail the corrective actions needed to resolve the issue. ### Risk No project is without risks. Within the CMMI process, these risks can be logged and tracked associated if necessary to the work items it relates to. Within the risk work item users are able to describe the risk, specify the probability as well as make contingency plans that allows everyone (with access) to know what to do in the event that this risk becomes an issue. Each of these work items have defined standard fields. However, there are also fields available which may not be on the form. The list of available fields which are available within the CMMI process template can be accessed via [Microsoft’s Work Item field index page](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/work-item-field?view=azure-devops). Additional information related to specified work items can also be found on the: - [Review Meeting Field Reference page](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/cmmi/guidance-review-meeting-field-reference-cmmi?view=azure-devops) - [Change Request Field Reference page](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/cmmi/guidance-change-request-field-reference-cmmi?view=azure-devops) - [Requirement Field Reference page](https://docs.microsoft.com/en-us/azure/devops/boards/work-items/guidance/cmmi/guidance-requirements-field-reference-cmmi?view=azure-devops) ### Useful fields There are some fields which tend to be under the radar but which are really useful during a project if used correctly. #### Activity The activity field is available on the Task work item and dictates the type of work being completed. E.g. the activity could relate to design, testing, documentation or development. This field ties closely to assessing the capacity of team members as team members can be assigned an amount of time for each activity. Populating the activity field on the task provides visibility of how much time has is needed for each type of activity. Previously, the activity field could not be modified in Azure DevOps. However, this is no longer the case. Values can be added/removed from this field without affecting the ability to view the capacity as previously described. #### Triage The triage field is available on the Bug, Change Request, Epic, Feature, Issue, Requirement and Task work item. When the work item is in a state of proposed, this field provides directions on what needs to be done. For e.g. A list of requirements which have been created during analysis may need to be reviewed before the analysis stage is complete. This field allows the team to have visibility of which requirements are pending information or has not yet been reviewed. #### Committed Requirements are often created in Azure DevOps before a decision is made on whether the business wishes to include them or not as this aids collaboration and allows an informed decision which can be looked back on if necessary. The committed field specifies if the requirement is within the scope of the project. #### Blocked Blocked is a standard field available on the Bug, Change Request, Requirement, Risk and Task work item and states that no progress can be made on the work item. An issue could be created and associated to the work item at this point. However, this is a suggested business process and not enforced by Azure DevOps. #### Subject Matter Expert Who are the subject matter experts to refer to in the business if there are questions related to a requirement? The Subject Matter Expert fields allows users to specify one or more team members to be consulted if necessary about the associated requirement. The team members specified must exist as users within Azure DevOps. #### Task Type The task type field is available on both the task and bug work item. It allows users to track work which was planned within the project vs work which has been added retrospectively to correct or mitigate issues. There are two additional work item templates available within Azure DevOps. The Agile Process and the Scrum Process. ## Overview of the Agile Process There are two process which are based on the agile methodology within Azure DevOps. The Agile and Scrum processes are similar but there are key differences which I’ve mentioned in my previous blog [introducing the Azure DevOps processes](https://triciasinclair.com/2019/12/02/azure-devops-processes-part-1/). This blog specifically aims to provide an overview of the agile process. The agile process is based on agile principles and allows users to track user stories and tasks at a more granular level than the Scrum process. The work items of note which are available as a part of the agile process are. ### Epics Epics within the Agile process are the same as Epics within the Basic and CMMI process. They contain the high level process of what is to be delivered. An example of this would be the delivery of different channels e.g phone or chat within a call centre. ### Feature Features are a grouping of functionality that provides business value when delivered. The Feature work item is the same as when used in a CMMI process. E.g telephony integration and call verification features would be required in order to deliver an epic related to the delivery of a telephone channel in a call centre. ### User Story First, user Stories work items are meant to capture the description of what is expected from the end user. User stories are typically captured in the format: As a \[user role\], I want \[goal\] so that \[defined reason\]. The user story work item also prompts for the acceptance criteria to be defined. The acceptance criteria documented in the user story is extremely important, as testers and developers rely on what is documented here to create test cases and solution designs. ![User Story Work Item](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/2_image.png?w=640&ssl=1)User Story Work Item ### Task As with the CMMI process, a task defines what needs to be completed in order to complete the user story. An example of a task which could be associated with the above requirement could be to create the credit limit field on the contact record. (Assuming you’re using a CDS database the name, address and date of birth fields should already exist). ### Issue The Issue work item in the agile process has the same purpose as the work item in the CMMI process. It relates to project related problems, allowing a resolution and plan to be available and visible to those within the project that need to have access to the information. ### Useful fields Some of the key fields to be aware of within the agile process are: #### Story Points Story Points are associated to user stories to indicates the effort needed to complete the user story. Populating this field allows the velocity to be tracked and enables the standard forecasting functionality. #### Acceptance Criteria The acceptance criteria attribute available on the user story work item allows users to define what conditions must be met in order for the user story to be accepted. #### Priority Setting the priority allows product owners to share what is important for the business and ensures that the user stories with the highest priorities are delivered quicker than those with a lower priority. If you have been following this series of blogs, you may notice that the Agile process shares many work items with the CMMI process. The image below shows how work items are different between the two processes. ![CMMI work items](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/2_image-1.png?w=640&ssl=1) CMMI work items ![Agile work items](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/2_image-2.png?w=640&ssl=1)Agile work items ## Overview of the Scrum Process The final process left to cover in this series of blogs is the Scrum process. In my previous blogs, I’ve detailed the differences between the Scrum and Agile process. This blog will provide an overview of the scrum process template. The Scrum process in Azure DevOps closely resembles the agile process. However, there are key differences. The Scrum process aligns to the Scrum framework and allows: - **Product Owners to manage their Product Backlog**. This includes setting the priority, order and business importance for each product backlog item. - The Development team to review work to be completed using tools such as the Sprint and Kanban board as well as the Sprint Backlog. - Having sprint planning and retrospectives using third party add-ons. I find this useful for teams which are not co-located Information about the scrum framework can be found on the [scrum.org website](https://www.scrum.org/resources/what-is-scrum). To manage scrum projects, users are able to create different work items provided in the scrum process template. The work items available in the scrum process are. ### Epic These are meant to contain details of the high level process being delivered. An example of a typical epic could be “Warranty Management”. The development team would be aware that warranty management may be needed but will require detailed requirements (created as product backlog items) in order to estimate and ultimately deliver the epic. ### Feature Features are a groupings of functionality that provide business value when delivered. Product Backlog Items which are focused on delivering the same functionality would likely belong to a feature. Creating and using available warranties could each by features associated to the epic described previously. ### Product Backlog Item Product Backlog Items define the detailed needs of a customer. These are typically written using the format: As a \[user role\], I want \[goal\] so that \[defined reason\] and should have clear and unambiguous acceptance criteria defined. These work items should be created and owned by the product owner. ### Task A task defines what needs to be completed in order to complete the product backlog item. An example of a task which could be associated with the above product backlog could be to create the create a warranty entity/table. ### Impediment Impediments in the scrum process allows the team to keep track of anything that is affecting their productivity or efficiency. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/3_image-2.png?w=640&ssl=1)Work Items Hierarchy in Scrum process ### Useful fields The work items described above contain fields which should be noted. Some of the key fields to be aware of within the Scrum process are: #### Priority Setting the priority allows product owners to share what is important for the business and ensures that the product backlog items with the highest priorities are delivered quicker than those with a lower priority. #### Effort The effort of a product backlog item can be defined to allow the scrum team to analyse if an item can be delivered within a sprint or even if the backlog item needs to be broken down further. The effort defined does not tend to relate to time. Instead, other strategies such as planning poker using the Fibonacci sequence can be used to populate the effort field. When the effort is populated, the product backlog will use the velocity, the requested order and the effort to show product backlog items which can naturally fit into the sprint. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/2_image-3.png?w=640&ssl=1)Effort shown on the Product Backlog #### Business Value The business value field allows the product owner to define how much value the item has to the company. An item that is assigned a higher number should be considered as having more business value than an item that is assigned a lower number. #### Acceptance Criteria The acceptance criteria attribute available on the product backlog item work item allows users to define what conditions must be met in order for the product backlog item to be accepted. #### Remaining Work This field displays the timed effort remaining to complete a task. If using the scrum process, the user will not have access to the original estimate and completed work fields. The remaining work field is used on the task board to track progress made by each team member. **Categories:** Azure DevOps, Tools **Tags:** agile, azure-devops, cmmi, processes, scrum --- ### [Testing APIs with RestClient in Visual Studio Code](https://puresourcecode.com/dotnet/net-core/testing-apis-with-restclient-in-visual-studio-code/) **Published:** February 17, 2022 **Author:** Enrico **Excerpt:** I show a simple way for Testing APIs with RestClient in Visual Studio Code. RestClient is a simple extension for Visual Studio Code. **Content:** In this new post, I show a simple way for testing APIs with RestClient in [Visual Studio](https://puresourcecode.com/category/tools/visual-studio-tools/) Code. This is for me the test API week and you will see few post about it. If you’ve been doing web development, you’re probably aware that a lot of our job revolves around data. Reading data, writing data, manipulating data and displaying it in the browser in a way that makes sense. And the vast majority of that data is supplied from **REST API** endpoints: **representational state transfer application programming interfaces** (what a mouth full, hence REST API). In laymen’s terms: the data we want exists in some other service or database, and our application queries that service to retrieve the data and use it as we see fit. ## My current tools Now, I think a lot of us are using [Postman](https://www.postman.com/) for check the APIs. I have to say that now Postman is becoming heavy and slow with a lot of functionalities. Most of the functionalities are there because they want to make money. Unfortunately, in my point of view, the tool is not fast and easy to use as before. So, I was looking around to find some simple alternative that I can use with my teams in an [agile environment](https://puresourcecode.com/dotnet/digital-transformation-scenario-azure-visual-studio-git/) with developers in other languages (like [R](https://puresourcecode.com/category/programming-languages/r/)) without knowledge in [.NET](https://puresourcecode.com/category/dotnet/). Also, I don’t want them to start to learn NUnit, XUnit or Microsoft Tests. Plus, I like to have an option to share with the team the tests and each person can run the tests. ### Visual Studio Code In every computer [Visual Studio Code](https://code.visualstudio.com/) is installed and this tool is very flexible, it is a good editor and every person knows how to use it. Also, it has a big marketplace. Why don’t look around and find something? ## Install RestClient After a quick search in the marketplace and look on the internet, I found RestClient, an extension to call APIs. It is each testing APIs with RestClient in Visual Studio Code, so let install it. To find it, open the marketplace extension in VS Code (the block of squares on the left panel), type “rest client” into the search bar, then install the first result in the list (the author should be Huachao Mao). Here’s a screenshot so you know you’ve gotten the right one. ![Visual Studio Code RestClient - Testing APIs with RestClient in Visual Studio Code](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/visual-studio-code-rest-client.png?resize=640%2C347&ssl=1)Visual Studio Code RestClient ## Start to use RestClient For that, simply create a file at the root of your project that ends in `.http`. REST Client recognizes this and knows it’s supposed to be able to run HTTP requests from this file. The REST Client plugin requires just a plain text file with the extension `.http` or `.rest`. The basic syntax is very simple as following ``` ### Request 1 [GET|POST] [REST API URL] # Request Line [Request Headers] [Request Body] ### Request 2 [GET|POST] [REST API URL] [Request Headers] [Request Body] ``` The file can contain multiple requests, each request is separated by `###` delimiter (three or more consecutive `#`). ## Request Line First, the first line of the request is the *Request Line*. It contains the request method (*GET* or *POST*), space, and then followed by the API URL endpoint. For example, I like to see the result of a public service from DB-IP that returns same information about a specific IP. So, in my file I’m going to write ``` GET https://api.db-ip.com/v2/free/86.170.154.104 ``` ![Send a request with RestClient - Testing APIs with RestClient in Visual Studio Code](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-15.png?resize=599%2C276&ssl=1)Send a request with RestClient Immediately after finishing to the URL with the verb, on top of it, a text appears: **Send Request**. If I click on it, the RestClient sends the request and in a new tab it shows the result ![RestClient shows all the details of the request - Testing APIs with RestClient in Visual Studio Code](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-16.png?resize=640%2C435&ssl=1)RestClient shows all the details of the request ## Request Header The lines immediately after the *Request Line* are *Request Header*. The supported syntax is the `field-name: field-value` format, each line represents one header. The Authentication also can be set in this Request Header section. ``` GET https://api.db-ip.com/v2/free/86.170.154.104 HTTP/1.1 Content-Type: application/json Authorization: Bearer ``` ## Variables You can declare your File variables to keep the values and reuse them in the script such as API base endpoint, version with `@variableName = variableValue` syntax in a separate line from request block. Then the request can use the defined variable with `{{variableName}}` syntax. Please note that you *do not* need the `""` or `''` characters for a string value. ``` @baseUrl = https://api.db-ip.com @version = 2 ### POST {{baseUrl}}/auth/oauth2/v{{version}}/token HTTP/1.1 ### @ip = 86.170.154.104 GET {{baseUrl}}/v{{version}}/{{ip}} ``` This is what I see in the Visual Studio Code and in the result. ![RestClient with parameters](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-17.png?resize=640%2C434&ssl=1)RestClient with parameters ## Variables: Request Variables You can declare *Request Variables* to get a request or response message content. The syntax is just `# @name requestName` on a line before the Request Line. Once the request is sent, the script can access response (or request) message information from `{{requestName.(response|request).(body|headers).(*|JSONPath|XPath|Header Name)}}` syntax. Using the example, I like to read the `countryName` from the result of the API and call another API that has a parameter the name of a country. Because I don’t have a similar API, I use the same API passing the `countryName` only to verify if the country is what I expect. So, the code I wrote is ``` @baseUrl = https://api.db-ip.com @version = 2 @ip = 86.170.154.104 # @name ipResponse GET {{baseUrl}}/v{{version}}/free/{{ip}} Accept: application/json #### Variable Response @countryName = {{ipResponse.response.body.$.countryName}} ### Test if the country name is passed GET {{baseUrl}}/v{{version}}/free/{{countryName}} ``` Now, the response from the first call will be placed in the variable `ipResponse`. Then, I can read the body and the value for `countryName`. For that, I wrote line 13. ![RestClient reads a value from the response](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-18.png?resize=640%2C432&ssl=1)RestClient reads a value from the response How you can see in the screenshot above, RestClient makes the api passing the value from the response. Obviously, in my case I didn’t expect a successful response but I verified the RestClient reads correctly a response and I can use it. ## Request Body The *Request Body* can be set by adding a *blank line* after Request Header and then place your HTTP request data in the next line. The request data can be various types of format such as JSON, XML, or key-value tuples. One example is the RDP Auth Service which use **application/x-www-form-urlencoded** Content-Type. This content-type uses key-value tuples separated by `&`, with a `=` between the key and the value. ## Send a POST request Testing APIs with RestClient in Visual Studio Code seems quite easy. What if I want to send a POST, PUT or DELETE request? Again, it is quite easy. I have only to specified the body like in this request: ``` POST https://localhost:3003/registerUser HTTP/1.1 content-type: application/json { "first_name": "Test", "last_name": "User", "email': "testing@email.com", "username": "rest-client-tester", "password": "testing123" } ``` Ok, let’s go over what’s happening in the code snippet above. The first thing REST Client needs in order to work, is the type of request to make and full URL path for the route its attempting to access. In this case, the request is a `POST` and the URL is `https://localhost:3003/registerUser`. The `HTTP/1.1` at the end of the first line has something to do with the standards established by [RFC 2616](https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html) but I’m not exactly sure if it’s necessary or not, so I left it there just to be safe. Then, since this is a `POST`, there’s a JSON body to include in the request, note that there’s a blank line between `Content-Type` and the body — this is intentional and required by REST Client. So we have the required fields filled out, and then, above the `POST` a little `Send Request` option should appear. Mouse over it and click, and see what comes back. ## Generate the code for the request Once you’ve finalized your request in RestClient extension, you might want to make the same request from your source code. It allows you to generate snippets of code in various languages and libraries that will help you achieve this. Once you prepared a request as previously, use shortcut `Ctrl+Alt+C`(`Cmd+Alt+C` for macOS), or right-click in the editor and then select `Generate Code Snippet` in the menu, or press `F1` and then select/type `Rest Client: Generate Code Snippet`, it will pop up the language pick list, as well as library list. After you selected the code snippet language/library you want, the generated code snippet will be previewed in a separate panel of Visual Studio Code, you can click the `Copy Code Snippet` icon in the tab title to copy it to clipboard. ![C# code generated from RestClient](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-19.png?resize=640%2C433&ssl=1)C# code generated from RestClient ## Wrap up In conclusion, this is how testing APIs with RestClient in Visual Studio Code. It seems to me quite easy for everybody to test any APIs. What do you think? Happy coding! **Categories:** .NET Core **Tags:** api, rest, testing, visual-studio, visualstudio-code --- ### [Remote debug Android devices](https://puresourcecode.com/dotnet/net-core/remote-debug-android-devices/) **Published:** February 10, 2022 **Author:** Enrico **Excerpt:** In this post, I show you how to remote debug live content on an Android device from your Windows, Mac, or Linux computer. Easy peasy **Content:** Creating my new [browser detect component](https://puresourcecode.com/dotnet/blazor/browser-detect-component-for-blazor/) from Blazor, the first issue I face was to find a way to check what errors I got when the JavaScript checks the capabilities of a device. In this post, I show you how to remote debug live content on an Android device from your Windows, Mac, or Linux computer. This tutorial teaches you how to: - Set up your Android device for remote debugging, and discover it from your development machine. - Inspect and debug live content on your Android device from your development machine. - Screencast content from your Android device onto a DevTools instance on your development machine. ## Step 1: Discover your Android device The workflow below works for most users. See [Troubleshooting: DevTools is not detecting the Android device](https://developer.chrome.com/docs/devtools/remote-debugging/#troubleshooting) for more help. 1. Open the **Developer Options** screen on your Android. See [Configure On-Device Developer Options](https://developer.android.com/studio/debug/dev-options.html). 2. Select **Enable USB Debugging**. 3. On your development machine, open Chrome. 4. Go to `chrome://inspect#devices`. 5. Make sure that the **Discover USB devices** checkbox is enabled.![The Discover USB Devices checkbox is enabled.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/4P4G0Hmt3CbDkqMoTOiY.png?resize=640%2C470&ssl=1) 6. Connect your Android device directly to your development machine using a USB cable. Your Android device may ask you to confirm that you trust this computer. The first time you do this, you usually see that DevTools has detected an offline device. If you see the model name of your Android device, then DevTools has successfully established the connection to your device. Continue to Step 2.![The Remote Target has successfully detected an offline device that is pending authorization.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/7Iy1yVH62Xz40tbiwgRg.png?resize=640%2C470&ssl=1)**Figure 3**. The **Remote Target** has successfully detected an offline device that is pending authorization 7. If your device is showing up as **Offline**, accept the **Allow USB Debugging** permission prompt on your Android device. ### Troubleshooting: DevTools is not detecting the Android device Make sure that your hardware is set up correctly: - If you’re using a USB hub, try connecting your Android device directly to your development machine instead. - Try unplugging the USB cable between your Android device and development machine, and then plugging it back in. Do it while your Android and development machine screens are unlocked. - Make sure that your USB cable works. You should be able to inspect files on your Android device from your development machine. Make sure that your software is set up correctly: - If your development machine is running Windows, try manually installing the USB drivers for your Android device. See [Install OEM USB Drivers](https://developer.android.com/tools/extras/oem-usb.html). - Some combinations of Windows and Android devices (especially Samsung) require extra set up. See [Chrome DevTools Devices does not detect device when plugged in](https://stackoverflow.com/questions/21925992). If you don’t see the **Allow USB Debugging** prompt on your Android device try: - Disconnecting and then re-connecting the USB cable while DevTools is in focus on your development machine and your Android homescreen is showing. In other words, sometimes the prompt doesn’t show up when your Android or development machine screens are locked. - Updating the display settings for your Android device and development machine so that they never go to sleep. - Setting Android’s USB mode to PTP. See [Galaxy S4 does not show Authorize USB debugging dialog box](https://android.stackexchange.com/questions/101933). - Select **Revoke USB Debugging Authorizations** from the **Developer Options** screen on your Android device to reset it to a fresh state. If you find a solution that is not mentioned in this section or in [Chrome DevTools Devices does not detect device when plugged in](https://stackoverflow.com/questions/21925992), please add an answer to that Stack Overflow question, or [open an issue in the webfundamentals repository](https://github.com/google/webfundamentals/issues/new?title=%5BRemote%20Debugging%5D)! ## Step 2: Debug content on your Android device from your development machine 1. Open Chrome on your Android device. 2. In the **`chrome://inspect/#devices`**, you see your Android device’s model name, followed by its serial number. Below that, you can see the version of Chrome that’s running on the device, with the version number in parentheses. Each open Chrome tab gets its own section. You can interact with that tab from this section. If there are any apps using WebView, you see a section for each of those apps, too. In **Figure 5** there are no tabs or WebViews open.![A connected remote device.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/y1pMOU5sSvCLPct4J480.png?resize=640%2C470&ssl=1)**Figure 4**. A connected remote device 3. In the **Open tab with url** text box, enter a URL and then click **Open**. The page opens in a new tab on your Android device. 4. Click **Inspect** next to the URL that you just opened. A new DevTools instance opens. The version of Chrome running on your Android device determines the version of DevTools that opens on your development machine. So, if your Android device is running a very old version of Chrome, the DevTools instance may look very different than what you’re used to. ### More actions: pause, focus, reload, or close a tab Below the URL you can find a menu to pause, focus, reload or close a tab. ![The menu for pausing, reloading, focusing, or closing a tab.](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/JaZPnZlDhAywdFkn8vNm.png?w=640&ssl=1)**Figure 5**. The menu for pausing, reloading, focusing, or closing a tab ### Inspect elements Go to the **Elements** panel of your DevTools instance, and hover over an element to highlight it in the viewport of your Android device. You can also tap an element on your Android device screen to select it in the **Elements** panel. Click **Select Element** on your DevTools instance, and then tap the element on your Android device screen. Note that **Select Element** is disabled after the first touch, so you need to re-enable it every time you want to use this feature. ### Screencast your Android screen to your development machine Click **Toggle Screencast** to view the content of your Android device in your DevTools instance. You can interact with the screencast in multiple ways: - Clicks are translated into taps, firing proper touch events on the device. - Keystrokes on your computer are sent to the device. - To simulate a pinch gesture, hold Shift while dragging. - To scroll, use your trackpad or mouse wheel, or fling with your mouse pointer. Some notes on screencasts: - Screencasts only display page content. Transparent portions of the screencast represent device interfaces, such as the Chrome address bar, the Android status bar, or the Android keyboard. - Screencasts negatively affect frame rates. Disable screencasting while measuring scrolls or animations to get a more accurate picture of your page’s performance. - If your Android device screen locks, the content of your screencast disappears. Unlock your Android device screen to automatically resume the screencast. **Categories:** .NET Core **Tags:** android, blazor, devtools, google-chrome, remote-debug --- ### [Deploy WordPress with Azure DevOps](https://puresourcecode.com/tools/deploy-wordpress-with-azure-devops/) **Published:** February 1, 2022 **Author:** Enrico **Excerpt:** In this new post, I show you how to deploy WordPress with Azure DevOps using an instance of WordPress app service created in the Azure **Content:** In this new post, I show you how to deploy [WordPress](https://wordpress.org/) with [Azure DevOps](https://puresourcecode.com/tag/azure-devops/) using an instance of [WordPress](https://puresourcecode.com/?s=wordpress) app service created in the [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/). ## WordPress on Azure First, in the Azure portal if we search for **WordPress**, with my surprise, we have different versions: - **WordPress** is an instance of App Service where [Azure](https://puresourcecode.com/dotnet/azure/azure-treasures-in-the-cloud/) installs WordPress. Also, the process creates a MySQL instance for it - **WordPress Server** is a virtual machine where Azure installs IIS and, under it. [PHP](https://puresourcecode.com/tag/php/) and then [WordPress](https://puresourcecode.com/tag/wordpress/). Then, **phpMyAdmin** to administer **MySQL**. It is possible to connect to this machine using RDP. So, in this post I use the first option. Now, I create a new resource, search for WordPress and select the first one (in the following screenshot). ![Create a resource in the Azure portal - Deploy WordPress with Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image.png?resize=640%2C319&ssl=1)Create a resource in the Azure portal Now, you see a page like the following screenshot where the WordPress resource is explain. To continue, press the **Create** button and follow the very simple instruction (just names). ![Start the creation of a new WordPress instance on the Azure portal - Deploy WordPress with Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-1.png?resize=640%2C411&ssl=1)Start the creation of a new WordPress instance on the Azure portal Then, at the end of the process, you have the credentials to access to your new MySQL. Save them because it won’t be easy to find or change them later. So, if everything is clear and ready, click on the **Create** button. ![Review of the creation of a new WordPress on the Azure portal - Deploy WordPress with Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-2.png?resize=640%2C779&ssl=1)Review of the creation of a new WordPress on the Azure portal After waiting few minutes that Azure is creating the resources, we are ready to the next step. ## Configure WordPress Now, that WordPress is installed from Azure, we are ready to configure it. So, open the website URL and start to configure your new instance of WordPress. First, select the language. ![WordPress configuration: language - Deploy WordPress with Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-3.png?resize=640%2C446&ssl=1)WordPress configuration: language Now, set the **Site name**, the admin username and the password and the admin email. Then, click on the button **Install WordPress**. ![WordPress configuration: blog details - Deploy WordPress with Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-4.png?resize=640%2C446&ssl=1)WordPress configuration: blog details After few seconds, the blog is ready to go and use and we see the following screenshot. ![WordPress configuration: success - Deploy WordPress with Azure DevOps](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-5.png?resize=640%2C446&ssl=1)WordPress configuration: success Now, we have to configura the App Service for Azure DevOps. ## Configure the App service So, now start the tricky part. We have to configure the App Service that contains WordPress to allow FTP. For that, click on **Configuration** from the menu on the left, then click on the tab **General settings** and then select **FTPS only** in the **FTP state**. ![Configuration for App Service](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-6.png?resize=640%2C408&ssl=1)Configuration for App Service With that, we can are ready to use the **Secure FTP** to upload files. But first, we need the credentials to connect to the FTPS. ## Update Deployment Center Now, again in Azure, click on **Deployment Center** on the left. So, under **FTPS credentials** you must copy **FTPS endpoint**, **Username** and **Password** to use in the pipeline in Azure DevOps. ![Configure the Deployment Center](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-7.png?resize=640%2C469&ssl=1)Configure the Deployment Center ## Configure the repository Now, we want to deploy WordPress with Azure DevOps. I create a new repository for my WordPress project. Via FTP I copied all the file and push them in the repository. ![Repository for WordPress](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-9.png?resize=296%2C846&ssl=1)Repository for WordPress Easy and straightforward. Nothing strange. The only thing we have to change is the `web.config`. By default, the website is not configure to provide fonts, videos or other type of files but HTML and CSS files. For that, we have to comunicate to IIS to provide some files with a specific [MIME type](https://puresourcecode.com/net-core/proper-mime-types-for-embedded-font-face-fonts/). Now, in the following web.config I configured to response videos (webm and ogv) and fonts (woff, woff2). with the right [MIME](https://en.wikipedia.org/wiki/Media_type). ``` ``` ## Add a new pipeline Finally, the last step: the configuration of the pipeline with [YAML](https://puresourcecode.com/tools/what-is-yaml/). Also, you can use a classic job but YAML is the way. ``` # Starter pipeline # Start with a minimal pipeline that you can customize to build and deploy your code. # Add steps that build, run tests, deploy, and more: # https://aka.ms/yaml trigger: - master pool: vmImage: ubuntu-latest steps: - task: FtpUpload@2 inputs: credentialsOption: 'inputs' serverUrl: '' username: '' password: '' rootDirectory: 'src' filePatterns: '**' remoteDirectory: '/site/wwwroot' clean: false cleanContents: false preservePaths: true trustSSL: false ``` Now, run the pipeline and it should work fine. When you copy the server URL from the Azure resource, the URL is ending with `/site/wwwroot`: remove it and place it in the `remoteDirectory` ## The final blog What does the blog look like? This is the result. Nothing that we haven’t seen. You can see it immediately after the first installation. But now, if you change the project, the blog will change. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/02/image-8.png?resize=640%2C446&ssl=1) ## Wrap up In conclusion, this is how to deploy WordPress with Azure DevOps. Is this post useful for you? Do you have any problem? How can I improve this post or the code in it? Please leave your [message](https://puresourcecode.com/forum/azure-configuration-and-deployment/). **Categories:** Azure, Azure DevOps, Tools **Tags:** azure-devops, deployment, wordpress --- ### [Employers are desperate for data scientists](https://puresourcecode.com/news/employers-are-desperate-for-data-scientists/) **Published:** January 31, 2022 **Author:** Enrico **Excerpt:** The demand for data science is growing fast and employers are desperate for data scientists. On top of that, the great resignations is on. What do you think about it? What is your experience? **Content:** The demand for data science is growing fast and employers are desperate for data scientists. Recruiters are struggling to find data scientists that can help them move their digital ambitions forward. Developer recruitment platforms report seeing a sharp rise in the demand for data science-related IT skills. The latest [IT Skills Report](https://devskiller.com/it-skills-report/) by developer screening and interview platform DevSkiller recorded a 295% increase in the number of data science-related tasks recruiters were setting for candidates in the interview process during 2021. So, this fuelled the growth of Python demand for which grew by 154%. Python is a programming language popular within data science and machine-learning applications. Data analysis was seen in 32.69% of data science coding tests. “The rise in popularity of data science comes as little surprise given how valuable data has become to companies across the globe,” said Jakub Kubryński, CEO at DevSkiller. “The number of tasks in our testing catalogue related to Data Science and Python grew in 2021 by 158.33% and 113.33% respectively.” ![Top IT Skills Report - Employers are desperate for data scientists](https://i0.wp.com/www.zdnet.com/a/img/resize/31a1d8271addaeb4e8de27bf48f2f8e7d0808bb4/2022/01/26/e969e877-e47b-4997-a351-db7b0174e074/devskiller-fastest-growing-skills.png?w=640&ssl=1)Top IT Skills Report DevSkiller’s findings were based on 102,869 coding tests sent through the DevSkiller platform between 1 December 2020 and 1 December 2021. The annual report aims to capture hiring trends from the global tech industry. For that, it is assessing the types of skills candidates are assessed on by recruiters. Cybersecurity and QA (quality assurance) were also among the fastest-growing skills, along with programming language PHP, Scala and Blockchain. ![Top IT Skills report: hiring insights - Employers are desperate for data scientists](https://i0.wp.com/www.zdnet.com/a/img/resize/dc1d48880446f6e84605ddbe9e708af72c71746e/2022/01/26/3c50552a-8df0-47b9-85be-a90f183beca2/devskiller-in-demand-skills.png?w=640&ssl=1)Top IT Skills report: hiring insights ## Tech talent patterns and trends in 2021 The pandemic of 2020 caused unprecedented disruption to the world. In 2021, the dust had finally started to settle and the figures emerged of the real effect on life, technology and business in the “new normal.” These changes and the challenges that came with them certainly make for interesting reading – not least in the world of technical recruitment. Dubbed the ‘Great Resignation,’ according to the latest [JOLTS report](https://www.statista.com/chart/26186/number-of-people-quitting-their-jobs-in-the-united-states/) (Job Openings and Labor Turnover Survey), 4.4 million Americans left their jobs in September 2021, compared to 2.1 million per month at the start of 2020. September being the sixth month in a row that the figures had continued to rise. ![The great resignation](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/1_IT-Skills-Report-BLS-graphic-1024x929.png?resize=640%2C581&ssl=1)The great resignation The figure for November itself, showed a record 4.5million workers quitting their jobs. About 3% of the total workforce. The reason for such a mass walkout was put down to people no longer wanting to work, so-called, “unreliable” positions that may leave them without a wage (or with a relatively small one) should another lockdown occur. But how has all of this affected the technical hiring industry, especially with so many of its workers operating remotely? ### The globalized IT workforce In recent years, demand for developers and engineers has soared in most areas of the technology industry. With more technical positions available now than ever before, developers can demand huge salaries. In particularly niche roles, DevOps for instance, wages have soared, as the amount of qualified candidates remains scarce. But the pandemic has had a huge effect on this too. For the first time ever, remote work is not only being talked about as a serious long-term option, but its popularity has also exponentially increased, due to the extremely rare working conditions brought about by the outbreak of COVID-19. This trend has had a knock-on effect on the tech talent landscape. Employers no longer need to search for qualified candidates ‘in their own backyard’. With online technical assessments, video interviews, and the ability to hold a conference call from one’s own home, technical recruiters have started to search for developers further afield, and with surprising results. There has been an increase in the number of western tech companies, making hires in countries that before, were not on their radar. ## Wrap up In conclusion, this is the actual trend right now where employers are desperate for data scientists and how to workforce is moving with the great resignation event. As a .NET/Microsoft developer, I see there are a lot of jobs for my skills and I think if a company wants a stable environment for sure invest in a [digital transformation](https://puresourcecode.com/dotnet/digital-transformation-scenario-azure-visual-studio-git/) and Microsoft tools and philosophy is the right and more convenient way. I like to read your comment about it. Please leave your message below or in the [forum](https://puresourcecode.com/forum/). **Categories:** News, Projects and ideas **Tags:** data-science, data-scientists, jobs, php, python **Hashtags:** data-science, data-scientist, jobs, php, recruiters --- ### [Google plans the kill cookies](https://puresourcecode.com/news/google-plans-the-kill-cookies/) **Published:** January 30, 2022 **Author:** Enrico **Excerpt:** Google’s plan to kill cookies means remove third-party cookies from Chrome hasn’t gone smoothly scrapping Federated Learning of Cohorts FLoC **Content:** Google’s plan to kill cookies means remove third-party cookies from [Chrome](https://puresourcecode.com/news/the-worlds-second-most-popular-os/) hasn’t gone smoothly. Back in January 2020 the company announced it would overhaul Chrome by removing cookies. Well, now it’s January 2022 and Google is back with another plan. This week the company announced it was scrapping Federated Learning of Cohorts (FLoC). This is a key part of its plan and it wants to replace it with a new system called **Topics**. ## Privacy Topics is just one element of Google’s wider [Privacy Sandbox](https://www.chromium.org/Home/chromium-privacy/privacy-sandbox/) plan to bring about the end of third-party cookies in Chrome. On the face of it, it’s a move to improve user privacy. But many privacy experts have argued that it’s impact will be limited. And even the ad tech industry [isn’t happy](https://arstechnica.com/gadgets/2021/04/everybody-hates-floc-googles-tracking-plan-for-chrome-ads/), with rivals arguing that Google is attempting to reshape online advertising in its image. In the third quarter of 2021 alone, the search giant made [$53 billion from advertising](https://abc.xyz/investor/static/pdf/2021Q3_alphabet_earnings_release.pdf?cache=f1ba3f6)—but the online world in which Google operates is changing. When it comes to limiting third-party cookies, Google is way behind its rivals. Safari, Firefox, and Brave have all restricted them for years. Apple’s Safari started doing so back in 2017. But what Google does will have by far the biggest impact. Chrome hogs [63 percent](https://gs.statcounter.com/browser-market-share) of the global browser market—meaning Google is likely to set a standard that others might be forced to follow. After failing with FLoC, the company is now presenting Topics as a different plan for the future of online advertising. Onlookers aren’t so sure. Topics works by analyzing your browsing history to work out the things you’re interested in. If you like cars, for example, Topics will show you adverts for cars on the websites that you visit. To work out that you like cars, each website that uses Google’s Topics API will be assigned an overall category. A website about tattooing, for instance, may fall into the body art category; a city newspaper would likely be assigned to the [local news category](https://github.com/jkarlin/topics/blob/main/taxonomy_v1.md). ## What’s next As you move around the web, Chrome will record the categories you visit the most. Then, each week, your five most popular categories will be gathered up—Google says this process is done on your device and not on its servers—and a sixth random topic will be added to add some noise in the system. These six categories are then shared with the websites you visit and are used to target the ads you see. The data is deleted after three weeks. **Categories:** Google, Google, News, Tools **Tags:** cookies, FLoC, google, google-chrome --- ### [World Map component for Blazor](https://puresourcecode.com/dotnet/blazor/world-map-component-for-blazor/) **Published:** January 29, 2022 **Author:** Enrico **Excerpt:** This World Map component for Blazor WebAssembly and Blazor Server creates an interactive map of the world or specific region and shows your data **Content:** This World Map component for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/) creates an interactive map of the world or specific region and shows your data. The component is built with .NET6 using [jqvmap](https://github.com/10bestdesign/jqvmap). `jQuery` is required. You can see the component is action on this [website](https://worldmap.puresourcecode.com/). The full source code is on [GitHub](https://github.com/erossini/BlazorWorldMap). For any comment or question, please use my [forum](https://puresourcecode.com/forum/) in the [WorldMap section](https://puresourcecode.com/forum/worldmap-for-blazor/). ## Add the component to your project First, you have to install the component from the [NuGet](https://www.nuget.org/packages/PSC.Blazor.Components.WorldMap/). Then, open your `index.html` or `_Host` and add at the end of the page the following scripts: ``` ``` `jQuery` is required, so I added it in the script section. The `src` could be different for you. Now, we have to tell the Blazor project we want to use the component. So, open your `_Import.razor` and add the following lines: ``` @using PSC.Blazor.Components.WorldMap @using PSC.Blazor.Components.WorldMap.Enums ``` Also, I recommend to add also the library [PSC.Extensions](https://puresourcecode.com/dotnet/net-core/a-lot-of-functions-for-net5/) to convert an `enum` in a string. So, add in the `_Import.razor` this line ``` @using PSC.Extensions ``` Awesome! We set the component and now we have to use it. ## Add a map Now, in your page you can display a map of a region or entire world. To display the map of the world, add the following code ``` ``` Now, you see the result of it in the following screenshot. ![Basic world map - World Map component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/image-5.png?resize=640%2C303&ssl=1)Basic world map Very basic but efficient. The magic is done for the component itself. When the first render of the component starts, the component itself is adding in the page the required CSS and JavaScript to display the map [using these scripts](https://puresourcecode.com/dotnet/blazor/dynamically-add-javascript-from-blazor-components/). Now, I like to display only the map of the USA. ``` ``` ![Basic USA map - World Map component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/image-6.png?resize=640%2C303&ssl=1)Basic USA map So, the component has 6 `enums` that contains all the regions: - Europe - Germany - Map - Russia - USA - World There is a long list of countries, regions and continent to display. Here the list Country**World** (default)AlgeriaArgentinaBrazilCanadaCroatiaFranceGermanyGreeceIndonesiaIranIraqPolandRussiaSerbiaTunisiaTurkeyUkraineUsaVenezuelaRegionEuropeFranceUSA countriesUSA DistrictsContinentAfricaAsiaAustraliaEuropeNorth AmericaSouth AmericaThe full list of country codes for all the maps is available in the component, in the [NuGet package page](https://www.nuget.org/packages/PSC.Blazor.Components.WorldMap/) or on [GitHub](https://github.com/erossini/BlazorWorldMap). ### Icons and flags Now, usually in connection with the map, we want to display flags or icons. For that, you can use other my component that allows you to use [SVG image for icons and flags](https://puresourcecode.com/dotnet/blazor/svg-icons-and-flags-for-blazor/). ## Add pins So, in a map we need to put pins. In this component I created 2 ways to have pins: by the `ID` (from the DOM) or to pass the HTML `content`. For each map you can choose only 1 option. ### Example of pin by ID ``` ``` Then, the code ``` @code { Dictionary pins = new Dictionary() { { "it", "svgIcon" } }; } ``` `SVGIcon` is the [component](https://puresourcecode.com/dotnet/blazor/svg-icons-and-flags-for-blazor/) for showing icons. `PinMode` is the property to select what kind of pin I want to display. In the `Dictionary` for the county, `it` I want to use the HTML element with ID `svgIcon`. ![Example with pins - World Map component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/image-7.png?resize=640%2C303&ssl=1)Example with pins ### Example of pin by content ``` ``` and then the code ``` @code { Dictionary pins = new Dictionary() { { "it", "\u003cimg src=\"pk.png\" width=\"30px\" /\u003e" } // content }; } ``` Obviously, you have to have the file `pk.png` in your `wwwroot` folder. ## Add data Now, we want to add data to our map. It is quite easy. In this case I have to pass to the component a `Dictionary` with the country code and the value. If you are not sure about the country code, you can use [PSC.Extensions](https://puresourcecode.com/dotnet/net-core/a-lot-of-functions-for-net5/) to have the right country code by the enum. Here an example ``` @page "/" Selected regions: @SelectedRegions Selection: @(String.Join(", ", Selection.ToArray())) @code { string SelectedRegions = ""; List Selection = new List(); Dictionary pins = new Dictionary() { { "it", "svgIcon" } // id }; //List EnabledRegions = new List() { "it", "au" }; Dictionary values = new Dictionary() { { World.UnitedStatesofAmerica.GetDescription(), "20937" }, { World.China.GetDescription(), "14723" }, { World.Japan.GetDescription(), "4975" }, { World.Germany.GetDescription(), "3846" }, { World.UnitedKingdom.GetDescription(), "2708" }, { World.France.GetDescription(), "2630" }, { World.India.GetDescription(),"2623" }, { World.Italy.GetDescription(), "1886" } }; Task OnRegionSelect(ChangeData data) { Console.WriteLine($"Selected {data.Code} / {data.Region}"); return Task.CompletedTask; } Task SelectedChanged(List slc) { Selection = slc; return Task.CompletedTask; } } ``` ## Wrap up In conclusion, we have a new beautiful World Map component for Blazor to use download and use. **Categories:** Blazor **Tags:** blazor-component, blazor-server, blazor-webassembly --- ### [Simple XML minifier in C#](https://puresourcecode.com/dotnet/net-core/simple-xml-minifier-in-c/) **Published:** January 28, 2022 **Author:** Enrico **Excerpt:** In this new post, I give you my code for a simple XML minifier in C#. I know I always have strange thought but I’m a developer **Content:** In this new post, I give you my code for a simple XML minifier in C#. I know I always have strange thought but I’m a developer… So, I was working on a new component for Blazor for [displaying icons and flags](https://puresourcecode.com/dotnet/blazor/svg-icons-and-flags-for-blazor/). The SVG images are a list of command for drawing a picture in a XML style. As every other XML, the code is indented. So, as humans we can read better the code. In my specific case, I want to have for each image a string and create a class with all the icons code in SVG (if it is not clear what I mean, see the [post](https://puresourcecode.com/dotnet/blazor/svg-icons-and-flags-for-blazor/)). The source code of this project is on [GitHub](https://github.com/erossini/SimpleMinifierXML). Please leave your comment below or use my [forum](https://puresourcecode.com/forum/). ## XMLMinifierSettings First, I create the class for `settings` and I define 4 properties for that. Then, I add 2 common settings: `Aggressive` to reduce as much as possible the XML and `NoMinification` that leave the XML as it is. You can create your own settings as you like. ``` public class XMLMinifierSettings { public bool RemoveEmptyLines { get; set; } public bool RemoveWhitespaceBetweenElements { get; set; } public bool CloseEmptyTags { get; set; } public bool RemoveComments { get; set; } public static XMLMinifierSettings Aggressive { get { return new XMLMinifierSettings { RemoveEmptyLines = true, RemoveWhitespaceBetweenElements = true, CloseEmptyTags = true, RemoveComments = true }; } } public static XMLMinifierSettings NoMinification { get { return new XMLMinifierSettings { RemoveEmptyLines = false, RemoveWhitespaceBetweenElements = false, CloseEmptyTags = false, RemoveComments = false }; } } } ``` ## XMLMinifier Now, the important part. The class `XMLMinifier` has a function `Minify` to organize the XML based on the settings. ``` public class XMLMinifier { private XMLMinifierSettings _minifierSettings; public XMLMinifier(XMLMinifierSettings minifierSettings) { _minifierSettings = minifierSettings; } public string Minify(string xml) { var originalXmlDocument = new XmlDocument(); originalXmlDocument.PreserveWhitespace = !(_minifierSettings.RemoveWhitespaceBetweenElements || _minifierSettings.RemoveEmptyLines); originalXmlDocument.Load(new MemoryStream(Encoding.UTF8.GetBytes(xml))); //remove comments first so we have less to compress later if (_minifierSettings.RemoveComments) { foreach (XmlNode comment in originalXmlDocument.SelectNodes("//comment()")) { comment.ParentNode.RemoveChild(comment); } } if (_minifierSettings.CloseEmptyTags) { foreach (XmlElement el in originalXmlDocument.SelectNodes("descendant::*[not(*) and not(normalize-space())]")) { el.IsEmpty = true; } } if (_minifierSettings.RemoveWhitespaceBetweenElements) { return originalXmlDocument.InnerXml; } else { var minified = new MemoryStream(); originalXmlDocument.Save(minified); return Encoding.UTF8.GetString(minified.ToArray()); } } } ``` ## Example Finally, a simple example how to use the code above. For the `path` folder, the console app reads the list of SVG file. Then, it sets the `XMLMinifier`. So, for each file it runs the Minify, remove the SVG tag and save the result string in a new file ending with `.min.svg` ``` using System.IO; using System.Linq; using System.Text.RegularExpressions; using XMLMinimizer; string path = @"C:\Users\enric\OneDrive\Desktop\fileToConvert"; DirectoryInfo d = new DirectoryInfo(path); //Assuming Test is your Folder FileInfo[] Files = d.GetFiles("*.svg"); //Getting Text files var xmlMin = new XMLMinifier(XMLMinifierSettings.Aggressive); string code = ""; foreach (FileInfo file in Files) { string text = System.IO.File.ReadAllText(file.FullName); string minText = xmlMin.Minify(text); string rsl = Regex.Replace(minText, "", "", RegexOptions.IgnoreCase); rsl = rsl.Replace("\"", "'"); string result = Path.GetFileNameWithoutExtension(file.FullName) + ".min" + Path.GetExtension(file.FullName); File.WriteAllText(Path.Combine(path, result), rsl); } ``` ## Wrap up In conclusion, this is the code for a simple XML minifier in C#. I hope you like and it could be useful for you. Happy coding! **Categories:** .NET Core, .NET6, C# **Tags:** c#, minifier, minimize, net6, netcore, svg, xml --- ### [SVG Icons and flags for Blazor](https://puresourcecode.com/dotnet/blazor/svg-icons-and-flags-for-blazor/) **Published:** January 27, 2022 **Author:** Enrico **Excerpt:** SVG Icons and flags for Blazor is a new library that contains tons of icons in SVG format and also all the world flags for each country **Content:** The component SVG Icons and flags for Blazor helps you to display a SVG image in your application for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/) with .NET6. All the icon are embedded in the component. Also, you have the complete SVG images for flags. So, to add an icon, the class `SVGIcons` has already 1298 SVG images ready to use. `SVGFlags` contains all the flags (534) in SVG in 2 formats: square and wide. The [NuGet package](https://www.nuget.org/packages/PSC.Blazor.Components.Icons/) for this component is available. ### OnClick[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=onclick) Now, this is an `EventCallback` if you want to receive a callback when you user clicks on the image. ### Color[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=color) The color of the SVG image. ### Size[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=size) The size of the image in pixel (default 24 pixels). This value is applied to `Height` and `Width`. So, the image is a square. ### StrokeWidth[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=strokewidth) The size of the pen to draw the image (default 2 pixels) ### Elements[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=elements) SVG image to display. If you have an SVG image pass only the content of SVG tag. ### Filled[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=filled) Define is you want to fill the image. ### Rotate[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=rotate) The rotation to apply to your image ### Examples[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=examples) #### Basic use ``` ``` #### Size and color ``` ``` ### CSS class[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=css-class) ``` ``` ## Flags This part of the component gives you the opportunity to display a flag in SVG format. ### OnClick[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=onclick) This is an `EventCallback` if you want to receive a callback when you user clicks on the image. ### FlagType[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=flagtype) The image could be: - Square - Wide ### Width[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=width) It is the width of the image. The default value is `120`. ### Height[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=height) It is the height of the image. The default value is `100`. ### CountryCode[](https://enricorossini.visualstudio.com/PureSourceCode/_git/PSC.Blazor.Components.Icons?anchor=countrycode) This is the code of the country with 2 letters. Country codeCountry nameacAscension IslandadAndorraaeUnited Arab EmiratesafAfghanistanagAntigua and BarbudaaiAnguillaalAlbaniaamArmeniaaoAngolaaqAntarcticaarArgentinaasAmerican SamoaatAustriaauAustraliaawArubaaxAland IslandsazAzerbaijanbaBosnia and HerzegovinabbBarbadosbdBangladeshbeBelgiumbfBurkina FasobgBulgariabhBahrainbiBurundibjBeninblSaint BarthélemybmBermudabnBrunei DarussalamboBolivia (Plurinational State of)bqBonaire, Sint Eustatius and SababrBrazilbsBahamasbtBhutanbvBouvet IslandbwBotswanabyBelarusbzBelizecaCanadaccCocos (Keeling) IslandscdDemocratic Republic of the CongocfCentral African RepubliccgRepublic of the CongochSwitzerlandciCôte d’IvoireckCook IslandsclChilecmCamerooncnChinacoColombiacpClipperton IslandcrCosta RicacuCubacvCabo VerdecwCuraçaocxChristmas IslandcyCyprusczCzech RepublicdeGermanydgDiego GarciadjDjiboutidkDenmarkdmDominicadoDominican RepublicdzAlgeriaeaCeuta & MelillaecEcuadoreeEstoniaegEgyptehWestern SaharaerEritreaesSpaines-ctCataloniaes-gaGaliciaetEthiopiaeuEuropefiFinlandfjFijifkFalkland IslandsfmFederated States of MicronesiafoFaroe IslandsfrFrancegaGabongbUnited Kingdomgb-engEnglandgb-nirNorthern Irelandgb-sctScotlandgb-wlsWalesgdGrenadageGeorgiagfFrench GuianaggGuernseyghGhanagiGibraltarglGreenlandgmGambiagnGuineagpGuadeloupegqEquatorial GuineagrGreecegsSouth Georgia and the South Sandwich IslandsgtGuatemalaguGuamgwGuinea-BissaugyGuyanahkHong KonghmHeard Island and McDonald IslandshnHondurashrCroatiahtHaitihuHungaryicCanary IslandsidIndonesiaieIrelandilIsraelimIsle of ManinIndiaioBritish Indian Ocean TerritoryiqIraqirIran (Islamic Republic of)isIcelanditItalyjeJerseyjmJamaicajoJordanjpJapankeKenyakgKyrgyzstankhCambodiakiKiribatikmComorosknSaint Kitts and NeviskpNorth KoreakrSouth KoreakwKuwaitkyCayman IslandskzKazakhstanlaLaoslbLebanonlcSaint LucialiLiechtensteinlkSri LankalrLiberialsLesotholgLGBTltLithuanialuLuxembourglvLatvialyLibyamaMoroccomcMonacomdMoldovameMontenegromfSaint MartinmgMadagascarmhMarshall IslandsmkNorth MacedoniamlMalimmMyanmarmnMongoliamoMacaumpNorthern Mariana IslandsmqMartiniquemrMauritaniamsMontserratmtMaltamuMauritiusmvMaldivesmwMalawimxMexicomyMalaysiamzMozambiquenaNamibiancNew CaledonianeNigernfNorfolk IslandngNigerianiNicaraguanlNetherlandsnoNorwaynpNepalnrNaurunuNiuenzNew ZealandomOmanpaPanamapePerupfFrench PolynesiapgPapua New GuineaphPhilippinespkPakistanplPolandpmSaint Pierre and MiquelonpnPitcairnprPuerto RicopsState of PalestineptPortugalpwPalaupyParaguayqaQatarreRéunionroRomaniarsSerbiaruRussiarwRwandasaSaudi ArabiasbSolomon IslandsscSeychellessdSudanseSwedensgSingaporeshSaint Helena, Ascension and Tristan da CunhasiSloveniasjSvalbard and Jan MayenskSlovakiaslSierra LeonesmSan MarinosnSenegalsoSomaliasrSurinamessSouth SudanstSao Tome and PrincipesvEl SalvadorsxSint MaartensySyrian Arab RepublicszSwazilandtaTristan da CunhatcTurks and Caicos IslandstdChadtfFrench Southern TerritoriestgTogothThailandtjTajikistantkTokelautlTimor-LestetmTurkmenistantnTunisiatoTongatrTurkeyttTrinidad and TobagotvTuvalutwTaiwantzTanzaniauaUkraineugUgandaumUnited States Minor Outlying IslandsunUnited NationsusUnited States of AmericauyUruguayuzUzbekistanvaHoly SeevcSaint Vincent and the GrenadinesveVenezuela (Bolivarian Republic of)vgVirgin Islands (British)viVirgin Islands (U.S.)vnVietnamvuVanuatuwfWallis and FutunawsSamoaxkKosovoxxUnknownyeYemenytMayottezaSouth AfricazmZambiazwZimbabwe### Example ``` ``` ### Screenshots ![Wide flags from the component - SVG Icons and flags for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/151448394-b32521b7-e06b-49ce-826e-d413cbdddb8e.png?w=640&ssl=1)Wide flags from the component ![Square flags from the component - SVG Icons and flags for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/151448495-f783f48e-9a36-4570-ae12-bd126c685e16.png?w=640&ssl=1)Square flags from the component ## Wrap up In conclusion, I hope you like this SVG Icons and flags for Blazor. I hope it is useful for all of us. Please extend this component: the full source code is on [GitHub](https://github.com/erossini/BlazorIconsAndFlags). I’m really happy if you want to leave a message about this component in the [forum](https://puresourcecode.com/forum/icons-and-flags-for-blazor/). **Categories:** Blazor **Tags:** blazor-component, blazor-server, blazor-webassembly --- ### [Code Snippet component for Blazor](https://puresourcecode.com/dotnet/blazor/code-snippet-component-for-blazor/) **Published:** November 24, 2021 **Author:** Enrico **Excerpt:** Today I want to show you how to create a Code Snipper component for Blazor using highlight.js that is available for 191 different languages **Content:** Today I want to show you how to create a Code Snippet component for [Blazor](https://puresourcecode.com/category/dotnet/blazor/) using [highlight.js](https://highlightjs.org/). Highlight.js is a syntax highlighting tool that is available for 191 different languages with 97 different styles. It works very well and the styles are great and it helps making this component extraordinarily simple. > I created a new component and you find all details on this [post](https://puresourcecode.com/dotnet/blazor/add-code-snippet-in-razor-pages/). ## Initializing the Javascript First, we are going to need to do is setup the JavaScript. [Highlight.js](https://highlightjs.org/) lets you include just the languages you need and for this we’re going to be using C#. Because of how Blazor renders, we’re also going to need a function we call in the `OnAfterRenderAsync` overload of our component. Here is the code with some context to see where I added the scripts. ``` Loading... An unhandled error has occurred. Reload 🗙 ``` As you can see, I added the `highlight.js` script, the C# language file and the CSS needed to put it all together. All the function I created does is tell `highlight.js` to find all the html tags I want to highlight and do it is magic with them. ## The CodeSnippet Component Now for the easy part, the Blazor component. Here is the code. ``` @ChildContent @code { [Inject] private IJSRuntime _js { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public string Language { get; set; } = "csharp"; protected override async Task OnAfterRenderAsync(bool firstRender) { await _js.InvokeVoidAsync("highlightSnippet"); } } ``` After looking at the markup, you can see that things are quite simple. We are using `@Language` as a parameter and defaulting that to `csharp` since that is what I mostly use the component for myself. The `@childContent` RenderFragment is where the code we put in our snippet component will be placed. The `OnAfterRenderAsync` overload is invoking our javascript function telling `highlight.js` to find our code and highlight it. It is as easy as that! You can also add a splash of your own CSS to make things a little prettier. Here is an example of how you could use the component, and the result from the page. The value for `Language` is the one of the supported languages and you find the list of this [page](https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md). A working example of this Code Snippet component for Blazor is available on my [website](https://datatable.puresourcecode.com/) for [DataTable](https://puresourcecode.com/dotnet/blazor/datatable-component-for-blazor/). ![Example of the result of the component - Code Snippet component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/image-10.png?resize=640%2C121&ssl=1)Example of the result of the component Happy coding! **Categories:** Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly --- ### [Add Code Snippet in Razor pages](https://puresourcecode.com/dotnet/blazor/add-code-snippet-in-razor-pages/) **Published:** January 26, 2022 **Author:** Enrico **Excerpt:** In this new post, I show you how to add code snippet in Razor pages for Blazor WebAssembly and Blazor Server using highlight.js **Content:** In this new post, I show you how to add code snippet in Razor pages for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). In the last few weeks, I’m on fire with Blazor! I love it! So, I created a Blazor component that is based on [highlight.js](https://highlightjs.org/). So, the goal of this new component is to help us to add in a Razor page a piece of code and color it with the color convention for the language using different style. For example, I want to display a code from [C#](https://puresourcecode.com/category/dotnet/csharp/) using the [Visual Studio](https://puresourcecode.com/category/tools/visual-studio-tools/) style. The result is like the following screenshot ![Code Snippet demo application - Add Code Snippet in Razor pages](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/image-4.png?resize=640%2C612&ssl=1)Code Snippet demo application Also, I want to add dynamically the required scripts and the CSS based on the language and the style I like to display. For [adding dynamically scripts and CSS](https://puresourcecode.com/dotnet/blazor/dynamically-add-javascript-from-blazor-components/), few days ago I created a post where I exactly explain how to do that (coincidence?) As usual, you have the full source code of this component on [GitHub](https://github.com/erossini/BlazorCodeSnippet) but please leave your comment at the bottom of this post or in the [forum](https://puresourcecode.com/forum/) in the section [CodeSnippet for Blazor](https://puresourcecode.com/forum/codesnippet-for-blazor/). Also, you can download a [NuGet package](https://www.nuget.org/packages/PSC.Blazor.Components.CodeSnippet/). ## Usage In your `Index.html` or `_Host` add this line ``` ``` Then, in your `_Imports.razor` add this line ``` @using PSC.Blazor.Components.CodeSnippet ``` Based on the parameters, the component adds automatically the required scripts and CSS in your page. ### [](https://github.com/erossini/BlazorCodeSnippet#add-a-codesnippet)Add a CodeSnippet For example, I want to add a C# code with the \**Visual Studio* Style. Between the `CodeSnippet` tag, you have to add the code you want to show. This is the code. ``` protected override async Task OnAfterRenderAsync(bool firstRender) { await _js.InvokeVoidAsync("loadJs", targetUrl); } ``` Then, I want to add a new CodeSnippet using XML and the style of **Android Studio**. You have to replace special characters like < (<) with the corrispondent HTML code. If you have multiple `CodeSnippet` in the same pag, you can avoid to load multiple times the `highlight.js` setting to `false` the parameter `LoadMainScript`. ``` ``` As a note, because this component is using [highlight.js](https://highlightjs.org/), you can choose between 196 programming languages with 243 styles! See the list of the full languages and styles on [GitHub](https://github.com/erossini/BlazorCodeSnippet#supported-languages). ## Wrap up In conclusion, add Code Snippet in Razor pages is very straight forward with my new component. Please leave your comment at the bottom of this post or in the [forum](https://puresourcecode.com/forum/) in the section [CodeSnippet for Blazor](https://puresourcecode.com/forum/codesnippet-for-blazor/). **Categories:** Blazor **Tags:** blazor-component, blazor-server, blazor-webassembly, code --- ### [Dynamically add JavaScript from Blazor components](https://puresourcecode.com/javascript/dynamically-add-javascript-from-blazor-components/) **Published:** January 25, 2022 **Author:** Enrico **Excerpt:** In this new post, I show you the code to dynamically add JavaScript from a Blazor components coming from the component itself or another URL **Content:** In this new post, I show you the code to dynamically add JavaScript from [Blazor](https://puresourcecode.com/tag/blazor/) [components](https://puresourcecode.com/tag/blazor-component/). For example, I’m creating a new component to display a world map. Based on the configuration, the component shows different maps: for example, the world map or the USA. So, every map is in a JavaScript file. I don’t want to force me as a developer to add the correct JavaScript; I want that the component does it for me. For this reason, I have to find a way to dynamically add [JavaScript](https://puresourcecode.com/category/javascript/) file from the Blazor component based on the configuration. Blazor allows adding script tags only to the root HTML document. This makes it difficult to add JavaScript files that are required only in a single or few components. ## Add Script to components So, to load a script file in a component, we should first, add a JavaScript function that loads the JavaScript from the specified URL to the main JavaScript file of the project. ### Create a script loader Now, create a new JavaScript file **script.js**. Then, add a new function that will load a script file from the specified URL. ``` function loadJs(sourceUrl) { if (sourceUrl.Length == 0) { console.error("Invalid source URL"); return; } var tag = document.createElement('script'); tag.src = sourceUrl; tag.type = "text/javascript"; tag.onload = function () { console.log("Script loaded successfully"); } tag.onerror = function () { console.error("Failed to load script"); } document.body.appendChild(tag); } ``` Add this script file to the root document `_Host.cshtml` or `Index.html`. This file will be located in the Shared folder. ``` ``` ### Inject IJSRuntime Now, go to the **Index.razor** component, or any other component where you want to load scripts dynamically inject the **IJSRuntime**. ``` @inject IJSRuntime _js ``` Then, override the **OnAfterRenderAsync** method and call the **loadJs** function. ``` protected override async Task OnAfterRenderAsync(bool firstRender) { var targetUrl = "/_content/yourcomponent/myjs.min.js"; await _js.InvokeVoidAsync("loadJs", targetUrl); } ``` In this example, we are loading a script from the component itself. Generally speaking, you can replace this URL with the URL to your JavaScript code. ### Invoke JS Using IJSRuntime Now, we have loaded a new script file dynamically from the component. Any function from the newly added script can be called with the help of `IJSRuntime`. ``` @page "/" @inject IJSRuntime _js; Hello, world! Welcome to your new app. Show Alert @code{ protected override async Task OnAfterRenderAsync(bool firstRender) { var targetUrl = "https://unpkg.com/sweetalert/dist/sweetalert.min.js"; await _js.InvokeVoidAsync("loadJs", targetUrl); } async Task Alert() { await _js.InvokeVoidAsync("swal", "Success!", "Script loaded", "success"); } } ``` I hope this post will help you. Happy coding. If you have any questions, let me know in the comments below. ## Add a CSS So, we saw how to dynamically add JavaScript from Blazor components. I can extend this code to add CSS to the page from Blazor components. This is the script. ``` function loadCSS(sourceUrl) { if (sourceUrl.Length == 0) { console.error("Invalid source URL"); return; } var link = document.createElement('link'); link.rel = "stylesheet"; link.type = "text/css"; link.href = sourceUrl; link.onload = function () { console.log("CSS loaded successfully"); } link.onerror = function () { console.error("CSS to load script"); } document.head.appendChild(link); } ``` ## More examples and documentation - [Write a reusable Blazor component](https://puresourcecode.com/dotnet/blazor/write-a-reusable-blazor-component/) - [Getting Started With C# And Blazor](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/) - [Setting Up A Blazor WebAssembly Application](https://puresourcecode.com/dotnet/blazor/setting-up-a-blazor-webassembly-application/) - [Working With Blazor Component Model](https://puresourcecode.com/dotnet/blazor/working-with-blazors-component-model/) - [Secure Blazor WebAssembly With IdentityServer4](https://puresourcecode.com/dotnet/blazor/secure-blazor-webassembly-with-identityserver4/) - [Blazor Using HttpClient With Authentication](https://puresourcecode.com/dotnet/blazor/blazor-using-httpclient-with-authentication/) - [InputSelect component for enumerations in Blazor](https://puresourcecode.com/dotnet/blazor/inputselect-component-for-enumerations-in-blazor/) - [Use LocalStorage with Blazor WebAssembly](https://puresourcecode.com/dotnet/blazor/use-localstorage-with-blazor-webassembly/) - [Modal Dialog component for Blazor](https://puresourcecode.com/dotnet/blazor/modal-dialog-component-for-blazor/) - [Create Tooltip component for Blazor](https://puresourcecode.com/dotnet/blazor/create-tooltip-component-for-blazor/) - [Consume ASP.NET Core Razor components from Razor class libraries | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/class-libraries?view=aspnetcore-5.0&tabs=visual-studio) **Categories:** Blazor, JavaScript **Tags:** blazor, blazor-server, blazor-webassembly, javascript --- ### [Minimal APIs in NET6](https://puresourcecode.com/dotnet/csharp/minimal-apis-in-net6/) **Published:** January 21, 2022 **Author:** Enrico **Excerpt:** From now on, we can create minimal APIs in NET6 that allows us to write in few lines of code powerful APIs. I collect all my understanding **Content:** From now on, we can create minimal [APIs](https://puresourcecode.com/category/dotnet/webapi/) in [NET6](https://puresourcecode.com/category/dotnet/net6/) that allows us to write in few lines of code powerful APIs. In this post, I collect all my understanding about this new powerful feature. The source code of this post is available on [GitHub](https://github.com/erossini/Net6MinimalAPIs). ## What Are Minimal APIs? So, the core idea behind minimal APIs is to remove some of the ceremony of creating simple APIs. It means defining lambda expressions for individual API calls. For example, this is as simple as it gets: ``` app.MapGet("/", () => "Hello World!"); ``` This code specifies a route (e.g., “/”) and a callback to execute once a request that matches the route and verb are matched. The method `MapGet` is specifically to map a `HTTP GET` to the callback function. So, much of the magic is in the type inference that’s happening. When we return a string (like in this example), it’s wrapping that in a `HTTP code 200` (e.g., OK) return result. How do you even call this? Effectively, these mapping methods are exposed. They’re extension methods on the `IEndpointRouteBuilder` interface. This interface is exposed by the `WebApplication` class that’s used to create a new Web server application in .NET 6. ## The New Program.cs Now, a lot has been written about the desire to take the boilerplate out of the startup experience in C# in general. To this end, Microsoft has added something called “Top Level Statements” to C# 10. This means that the `program.cs` that you rely on to start your Web applications don’t need a `void Main()` to bootstrap the app. It’s all implied. Before C# 10, a startup looked something like this: ``` using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; namespace Client.Api { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } } ``` The need for a class and a `void Main` method that bootstraps the host to start the server is how we’ve been writing ASP.NET in the .NET Core way for a few years now. Now, in minimal APIs in NET6 there is the introduction of **top-level statements**, they want to streamline this boilerplate, as seen below: ``` var builder = WebApplication.CreateBuilder(args); // Setup Services var app = builder.Build(); // Add Middleware // Start the Server app.Run(); ``` Instead of a `Startup` class with places to set up services and middleware, it’s all done in this very simple top-level program. What does this have to do with Minimal APIs? The app that the builder object builds support the `IEndpointRouteBuilder` interface. So, in our case, the set up to the APIs is just the middleware: ``` var builder = WebApplication.CreateBuilder(args); // Setup Services var app = builder.Build(); // Map APIs app.MapGet("/", () => "Hello World!"); // Start the Server app.Run(); ``` ## Routing The first thing you might notice is that the pattern for mapping API calls looks a lot like MVC controllers’ pattern matching. This means that Minimal APIs look a lot like controller methods. For example: ``` app.MapGet("/api/clients", () => new Client() { Id = 1, Name = "Client 1" }); app.MapGet("/api/clients/{id:int}", (int id) => new Client() { Id = id, Name = "Client " + id }); ``` Simple paths like the `/api/clients` point at simple URI paths, whereas using the parameter syntax (even with constraints) continues to work. Notice that the callback can accept the ID that’s mapped from the URI just like MVC controllers. One thing to notice in the lambda expression is that the parameter types are inferred (like most of C#). This means that because you’re using a URL parameter (e.g., `id`), you need to type the first parameter. If you didn’t type it, it would try to guess the type in the lambda expression: ``` app.MapGet("/api/clients/{id:int}", (id) => new Client() { Id = id, // Doesn't Work Name = "Client " + id }); ``` This doesn’t work because without the hint of type, the first parameter of the lambda expression is assumed to be an instance of `HttpContext`. That’s because, at its lowest level, you can manage your own response to any request with the context object. But for most of you, you’ll use the parameters of the lambda expression to get help in mapping objects and parameters. ## Using Services So far, the APIs calls you’ve seen aren’t anything like real world. In most of those cases, you want to be able to use common services to execute calls. This brings me to how to use Services in Minimal APIs in NET6. You may have noticed earlier that I’d left a space to register services before I built the `WebApplication`: ``` var builder = WebApplication.CreateBuilder(args); // Register services here var app = builder.Build(); ``` You can just use the builder object to access the services, like so: ``` var builder = WebApplication.CreateBuilder(args); // Register services builder.Services.AddDbContext(); builder.Services.AddTransient(); var app = builder.Build(); ``` Here you can see that you can use the `Services` object on the application builder to add any services you need (in this case, I’m adding an Entity Framework Core context object and a repository that I’ll use to execute queries. To use these services, you can simply add them to the lambda expression parameters to use them: ``` app.MapGet("/clients", async (IClientRepository repo) => { return await repo.GetClientsAsync(); }); ``` By adding the required type, it will be injected into the lambda expression when it executes. This is unlike controller-based APIs in that dependencies are usually defined at the class level. These injected services don’t change how services are handled by the service layer (i.e., Minimal APIs still create a scope for scoped services). When you’re using URI parameters, you can just add the services required to the other parameters. For example: ``` app.MapGet("/clients/{id:int}", async (int id, IClientRepository repo) => { return await repo.GetClientAsync(id); }); ``` This requires you think about the services you require for each API call separately. But it also provides the flexibility to use services at the API level. ## Verbs So far, all I’ve looked at are HTTP GET APIs. There are methods for the different types of verbs. These include: - MapPost - MapPut - MapDelete These methods work identically to the `MapGet` method. For example, take this call to `POST` a new client: ``` app.MapPost("/clients", async (Client model, IClientRepository repo) => { // ... }); ``` Notice that the model in this case doesn’t need to use attributes to specify **FromBody.** It infers the type if the shape matches the type requested. You can mix and match all of what you might need (as seen in `MapPut`): ``` app.MapPut("/clients/{id}", async (int id, ClientModel model, IClientRepository repo) => { // ... }); ``` For other verbs, you need to handle mapping of other verbs using MapMethods: ``` app.MapMethods("/clients", new [] { "PATCH" }, async (IClientRepository repo) => {return await repo.GetClientsAsync(); }); ``` Notice that the `MapMethods` method takes a path, but also takes a list of verbs to accept. In this case, I’m executing this lambda expression when a PATCH verb is received. Although you’re creating APIs separately, most of the same code that you’re familiar with will continue to work. The only real change is how the plumbing finds your code. ## Using HTTP Status Codes In these examples, so far, you haven’t seen how to handle different results of an API action. In most of the APIs I write, I can’t assume that it succeeds, and throwing exceptions isn’t the way that I want to handle failure. To that end, you need a way of controlling what status codes to return. These are handled with the `Results` static class. You simply wrap your result with the call to `Results` and the status code: ``` app.MapGet("/clients", async (IClientRepository repo) => { return Results.Ok(await repo.GetClientsAsync()); }); ``` Results supports most status codes you’ll need, like: - **Results.Ok:** 200 - **Results.Created:** 201 - **Results.BadRequest:** 400 - **Results.Unauthorized:** 401 - **Results.Forbid:** 403 - **Results.NotFound:** 404 - Etc. In a typical scenario, you might use several of these: ``` app.MapGet("/clients/{id:int}", async (int id, IClientRepository repo) => { try { var client = await repo.GetClientAsync(id); if (client == null) { return Results.NotFound(); } return Results.Ok(client); } catch (Exception ex) { return Results.BadRequest("Failed"); } }); ``` If you’re going to pass in a delegate to the `MapXXX` classes, you can simply have them return an `IResult` to require a status code: ``` app.MapGet("/clients/{id:int}", HandleGet); async Task HandleGet(int id, IClientRepository repo) { try { var client = await repo.GetClientAsync(id); if (client == null) return Results.NotFound(); return Results.Ok(client); } catch (Exception) { return Results.BadRequest("Failed"); } } ``` Notice that because you’re `async` in this example, you need to wrap the `IResult` with a `Task` object. The resulting return is an instance of `IResult`. Although Minimal APIs are meant to be small and simple, you’ll quickly see that, pragmatically, APIs are less about how they’re instantiated and more about the logic inside of them. Both Minimal APIs and controller-based APIs work essentially the same way. The plumbing is all that changes. ## Securing Minimal APIs Although Minimal APIs in NET6 work with authentication and authorization middleware, you may still need a way to specifying, on an API-level, how security should work. If you’re coming from controller-based APIs, you might use the `Authorize` attribute to specify how to secure your APIs, but without controllers, you’re left to specify them at the API level. You do this by calling methods on the generated API calls. For example, to require authorization: ``` app.MapPost("/clients", async (ClientModel model, IClientRepository repo) => { // ... }).RequireAuthorization(); ``` This call to `RequireAuthorization` is tantamount to using the `Authorize` filter in controllers (e.g., you can specify which authentication scheme or other properties you need). Let’s say you’re going to require authentication for all calls: ``` builder.Services.AddAuthorization(cfg => { cfg.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); }); ``` You’d then not need to add `RequireAuthentication` on every API, but you could override this default by allowing anonymous for other calls: ``` app.MapGet("/clients", async (IClientRepository repo) => { return Results.Ok(await repo.GetClientsAsync()); }).AllowAnonymous(); ``` In this way, you can mix and match authentication and authorization as you like. ## Wrap up With this post, I introduced minimal APIs in NET6. But it is not finish. There are at least other 2 important thing to learn: - add Swagger to the project - how to test the APIs Now, you find on [GitHub](https://github.com/erossini/Net6MinimalAPIs) the full source code of this post with already Swagger installed and 2 test projects: one for xUnit and one for NUnit. In the next posts, I will explain how. **Categories:** .NET6, C# **Tags:** api, net6, webapi --- ### [Uploading files in ASPNET Core](https://puresourcecode.com/dotnet/net-core/uploading-files-in-aspnet-core/) **Published:** January 20, 2022 **Author:** Enrico **Excerpt:** Uploading files in ASP.NET Core is largely the same as standard full framework MVC but now we can stream large files. Here I explain how **Content:** Uploading files in ASPNET Core is largely the same as standard full framework MVC, but now we can stream large files. We will go over both methods of uploading a file in ASP.NET Core. The source code of this post is on [GitHub](https://github.com/erossini/ASPNETCoreUploadFiles). I wrote another post about [Upload/Download Files using HttpClient](https://puresourcecode.com/dotnet/net6/upload-download-files-using-httpclient/) that maybe can interest you. ## Model Binding IFormFile (Small Files) So, when uploading a file via this method, the important thing to note is that your files are uploaded in their entirety before execution hits your controller action. What this means is that the disk on your server holds a temporary file while you decide where to push it. With these small files this is fine, larger files you run into issues of scale. If you have many users all uploading large files you are liable to run out of ram (where the file is stored before moving it to disk), or disk space itself. For your HTML, it should look something like this: ``` Upload one or more files using this form: ``` The biggest thing to note is that the encoding type is set to `multipart/form-data`, if this is not set then you will go crazy trying to hunt down why your file is showing up in your controller. Your controller action is actually very simple. It will look something like: ``` [HttpPost] public IActionResult Index(List files) { //Do something with the files here. return Ok(); } ``` Note that the name of the parameter “files” should match the name on the input in HTML. Other than that, you are there and done. There is nothing more than you need to do. ## Streaming Files (Large Files) Now, for large files, instead of buffering the file in its entirety, you can stream the file upload. This introduces challenges as you can no longer use the built-in model binding of ASP.NET Core. Various tutorials out there show you how to get things working with massive pieces of code, but I’ll give you a helper class that should alleviate most of the work. Most of this work is taken from Microsoft’s tutorial on file uploads [here](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads). Unfortunately, it’s a bit all over the place with helper classes that you need to dig around the web for. First, take this helper class and stick it in your project. This code is taken from a Microsoft project [here.](https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/models/file-uploads/samples/5.x/LargeFilesSample) ### MultipartRequestHelper code ``` public static class MultipartRequestHelper { // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" // The spec says 70 characters is a reasonable limit. public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) { // .NET Core lengthLimit) { throw new InvalidDataException( $"Multipart boundary length limit {lengthLimit} exceeded."); } return boundary; } public static bool IsMultipartContentType(string contentType) { return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; } public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition) { // Content-Disposition: form-data; name="key"; return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") && string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core p.ParameterType == typeof(IFormFile)); operation.RequestBody.Content[fileUploadMime].Schema.Properties = fileParams.ToDictionary(k => k.Name, v => new OpenApiSchema() { Type = "string", Format = "binary" }); } } ``` The only thing left is to plug in this operation class to our Web API DI container registration in `Startup.cs` ``` public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Sample.FileUpload.Api", Version = "v1" }); c.OperationFilter(); }); } ``` Now, once we start the service and access the Swagger UI, we’ll get the proper UI and you can select the file from local machine and test the endpoint directly from Swagger UI. ![Upload file from Swagger - Uploading files in ASPNET Core](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/image-2.png?resize=640%2C367&ssl=1)Upload file from Swagger ## Wrap up In conclusion, we saw the way for uploading files in ASPNET Core using .NET Core version 2 or 3 or the new version 5 or 6. **Categories:** .NET Core, ASP.NET, Blazor, C#, MVC, WebAPI **Tags:** aspnet-5, aspnet-core, upload, upload-big-files, webapi --- ### [Google Analytics illegal in Austria](https://puresourcecode.com/news/google-analytics-illegal-in-austria/) **Published:** January 20, 2022 **Author:** Enrico **Excerpt:** Google Analytics illegal in Austria: Austria has ruled that Austrian website providers using Google Analytics are in violation of the GDPR.  **Content:** Google Analytics illegal in Austria. The Austrian Data Protection Authority (“Datenschutzbehörde” or “DSB” or “DPA”) has ruled that Austrian website providers using Google Analytics are in violation of the GDPR. This ruling stems from a decision made in 2020 by the [Court of Justice of the European Union](https://ec.europa.eu/info/law/law-topic/data-protection/international-dimension-data-protection/eu-us-data-transfers_en#:~:text=Commercial%20sector%3A%20EU%2DUS%20Privacy%20Shield,-The%20adequacy%20decision&text=This%20framework%20protects%20the%20fundamental,US%20under%20the%20Privacy%20Shield.) (CJEU) that stated that cloud services hosted in the US are incapable of complying with the GDPR and EU privacy laws. The decision was made because of the US surveillance laws requiring US providers (like Google or Facebook) to provide personal data to US authorities. The 2020 ruling, known as “Schrems II”, marked the ending of the Privacy Shield, a framework that allowed for EU data to be transferred to US companies that became certified. The tech industry was sent into a frenzy following this decision, but many US and EU companies decided to ignore the case. The choice to ignore is what landed one Austrian business in the DPA’s line of fire, damaging the brand’s reputation and possibly resulting in a hefty fine of up to €20 million or 4% of the organisation’s global turnover. ## About the Austrian DPA’s Model Case In this specific case, [noyb](https://noyb.eu/en) (the European Center for Digital Rights) found that IP addresses (which are classified as personal data by the GDPR) and other identifiers were sent to the US in cookie data as a result of the organisation using Google Analytics. This model case led to the DPA’s decision to rule that Austrian website providers using Google Analytics are in violation of GDPR. It is believed that other EU Member States will soon follow in this decision as well. “We expect similar decisions to now drop gradually in most EU member states. We have filed 101 complaints in almost all Member States and the authorities coordinated the response. A similar decision was also issued by the European Data Protection Supervisor last week.” Max Schrems, honorary chair of noyb.eu ## What does this mean if you are using Google Analytics? If there is one thing to learn from this case, it is that ignoring these court rulings and continuing to use Google Analytics is not a viable option. If you are operating a website in Austria, or your website services Austrian citizens, you should remove Google Analytics from your website immediately. For businesses in other EU Member States, it is also highly recommended that you take action before noyb and local data protection authorities start targeting more businesses. “Instead of actually adapting services to be GDPR compliant, US companies have tried to simply add some text to their privacy policies and ignore the Court of Justice. Many EU companies have followed the lead instead of switching to legal options.” Max Schrems **Categories:** Google, News **Tags:** europe, gdpr, google, google-analytics --- ### [Microsoft to acquire Activision Blizzard](https://puresourcecode.com/news/microsoft-to-acquire-activision-blizzard/) **Published:** January 19, 2022 **Author:** Enrico **Excerpt:** Microsoft is acquiring Activision Blizzard, the publisher of Call of Duty, World of Warcraft, and Diablo. The deal will value at $68.7billion **Content:** [Microsoft is acquiring Activision Blizzard](https://news.xbox.com/en-us/2022/01/18/welcoming-activision-blizzard-to-microsoft-gaming/), the troubled publisher of Call of Duty, World of Warcraft, and Diablo. The deal will value Activision at $68.7 billion, far in excess of the $26 billion Microsoft paid to acquire LinkedIn in 2016. It’s Microsoft’s biggest push into gaming, and the company says it will be the “third-largest gaming company by revenue, behind Tencent and Sony” once the deal closes. ## More on Xbox Game Pass Microsoft plans to add many of Activision’s games to [Xbox](https://puresourcecode.com/tag/xbox/) Game Pass once the deal closes. With the acquisition of Activision, Microsoft will soon publish franchises like Warcraft, Diablo, Overwatch, Call of Duty, and Candy Crush. “Upon close, we will offer as many Activision Blizzard games as we can within Xbox Game Pass and PC Game Pass, both new titles and games from Activision Blizzard’s incredible catalog,” says Microsoft’s CEO of gaming Phil Spencer. Xbox Game Pass now has 25 million subscribers, as Microsoft continues to acquire studios to boost the subscription service. ![Microsoft to acquire Activision Blizzard](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/blizzard.jpg?resize=640%2C427&ssl=1)Microsoft to acquire Activision Blizzard ## Microsoft vision Microsoft doesn’t detail exactly how it will approach solving these issues, and the company says Bobby Kotick will continue to serve as CEO of Activision Blizzard for now. It looks like Kotick won’t remain once the deal is fully closed and after the transition period to Microsoft, though. Spencer, formerly head of gaming at Microsoft, is now CEO of Microsoft Gaming, and the company says the Activision Blizzard business will report directly to Spencer. “As a company, Microsoft is committed to our journey for inclusion in every aspect of gaming, among both employees and players,” says Spencer. “We deeply value individual studio cultures; also, we believe that creative success and autonomy go hand-in-hand with treating every person with dignity and respect. We hold all teams, and all leaders, to this commitment. We’re looking forward to extending our culture of proactive inclusion to the great teams across Activision Blizzard.” Microsoft’s huge Activision Blizzard deal comes nearly a year after the company acquired Bethesda (ZeniMax Media) for $7.5 billion. At the time, that acquisition bolstered the company’s first-party Xbox game studios to a total of 23 and was seen as a huge boost for Xbox Game Pass. **Categories:** Microsoft, News **Tags:** blizzard, candy-crush, diablo, games, xbox, xbox-series-s --- ### [Markdown editor with Blazor](https://puresourcecode.com/dotnet/html/markdown-editor-with-blazor/) **Published:** November 5, 2021 **Author:** Enrico **Excerpt:** In this new post, I will show you have to create a simple Markdown editor component for Blazor Assembly and Blazor Server. Source code inside. **Content:** In this new post, I will show you have to create a simple Markdown editor [component](https://puresourcecode.com/tag/blazor-component/) for [Blazor Assembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/). The source code of this component with an example is on [GitHub](https://github.com/erossini/BlazorMarkdownEditor). If you are looking for more examples of components, here some more links: - [Using Chart.Js With Blazor](https://puresourcecode.com/dotnet/blazor/using-chart-js-with-blazor/) - [Create An Accordion Component With Blazor](https://puresourcecode.com/dotnet/blazor/create-an-accordion-component-with-blazor/) - [Create a Blazor component for Quill](https://puresourcecode.com/dotnet/blazor/create-a-blazor-component-for-quill/) - [Segment control for Blazor](https://puresourcecode.com/dotnet/blazor/segment-control-for-blazor/) - [Tabs control for Blazor](https://puresourcecode.com/dotnet/blazor/tabs-control-for-blazor/) > In January 2022 I completely rewrite this component. Now, the [Markdown Editor for Blazor](https://puresourcecode.com/dotnet/blazor/markdown-editor-component-for-blazor/) is a very powerful and complete component with upload images. The final result of this component in an application is like the following screenshots. In the **Write** tab, you type your text in Markdown format. When you click on the **Preview** tab, you have the text in HTML. ![Write your Markdown text - Markdown editor with Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/140496482-719e6b90-dcee-4547-b6b1-e5a4c7836e77.png?w=640&ssl=1)Write your Markdown text ![Markdown preview - Markdown editor with Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/140496580-47569d20-ff3f-4f57-bd03-98e3ac0906ba.png?w=640&ssl=1)Markdown preview ## The code So, as usual I have to create a new project for the component and create a new Razor page and its name is `MarkdownEditor.razor`. The HTML for this page is the following code ``` @using System.Linq.Expressions Write Preview @if (EnableToolbar) { } @if (isWriteActive) { Learn more about MarkDown here. @if (showHelp) { Close Help } } else { @((MarkupString)_previewText) } ``` Then, the code section in the page is: ``` @code { [Parameter] public string Value { get; set; } [Parameter] public EventCallback ValueChanged { get; set; } [Parameter] public Expression ValueExpression { get; set; } [Parameter] public bool EnableToolbar { get; set; } = true; [Parameter] public string id { get; set; } [CascadingParameter] private EditContext CascadedEditContext { get; set; } private bool isWriteActive = true; private string _previewText = ""; private int _rows = 6; private bool showHelp = false; private FieldIdentifier _fieldIdentifier; private string _fieldCssClasses => CascadedEditContext?.FieldCssClass(_fieldIdentifier) ?? ""; protected override void OnInitialized() { _fieldIdentifier = FieldIdentifier.Create(ValueExpression); } private void CalculateSize(string value) { _rows = Math.Max(value.Split('\n').Length, value.Split('\r').Length); _rows = Math.Max(_rows, 6); } private void HandleHelpClick() { showHelp = true; } private void HandleCloseHelpClick() { showHelp = false; } private async Task HandleInput(ChangeEventArgs args) { CalculateSize(args.Value.ToString()); await ValueChanged.InvokeAsync(args.Value.ToString()); CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier); _previewText = MarkdownParser.Parse(args.Value.ToString()); } private void UpdatePreview() { _previewText = MarkdownParser.Parse(Value.ToString()); } private void HandleBoldClick() { Value = $"{Value} **(Bolded Text Here)**"; UpdatePreview(); } private void HandleItalicClick() { Value = $"{Value} *(Italic Text Here)*"; UpdatePreview(); } private void HandleListClick() { Value = $"{Value} \n - List Item"; UpdatePreview(); } private void HandleWriteClick() { isWriteActive = true; } private void HandlePreviewClick() { isWriteActive = false; } } ``` ### MarkdownParser ``` internal static class MarkdownParser { internal static string Parse(string value) { if (!string.IsNullOrEmpty(value)) { var pipeline = new MarkdownPipelineBuilder() .UseEmojiAndSmiley() .UseAdvancedExtensions() .Build(); return Markdown.ToHtml(value, pipeline); } return ""; } } ``` ## Usage Now, to convert Markdown in HTML, I’m adding [Markdig](https://github.com/lunet-io/markdig) from a NuGet package with ``` Install-Package Markdig ``` So, add the Editor to your `_Imports.razor` ``` @using PSC.Blazor.Components.MarkdownEditor ``` Then, inside of an `EditForm` reference the editor component and bind it. ``` ``` The editor binds the markdown text, not parsed HTML. The toolbar is added by default. You can disable this by passing `EnableToolbar="false"` into the component. ## Wrap up In conclusion, I created a component for a Markdown editor with Blazor. Please leave your comment below or in the [forum](https://puresourcecode.com/forum/). **Categories:** Blazor, HTML **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly --- ### [Xbox One consoles discontinued](https://puresourcecode.com/news/xbox-one-consoles-discontinued/) **Published:** January 14, 2022 **Author:** Enrico **Excerpt:** Microsoft has officially confirmed that the manufacturing of all Xbox One consoles has ceased in favour of Xbox Series X and Xbox Series S **Content:** Microsoft has officially confirmed that [Xbox One](https://puresourcecode.com/tag/xbox/) consoles discontinued in favour of Xbox Series S and Xbox Series S. So, the manufacturing of all Xbox One consoles has ceased. In fact, it’s not even something that’s only just now happened. According to Xbox, production on all Xbox One consoles – all versions of it – was stopped by the end of 2020. As for why, the answer is simple: to focus on the Xbox Series X and Xbox Series S consoles. Interestingly, this seems to be the opposite tact of PlayStation, which has reportedly increased production on the PlayStation 4 due to shortages of the PlayStation 5. “To focus on production of Xbox Series X / S, we stopped production for all Xbox One consoles by the end of 2020,” said Cindy Walker, senior director of Xbox console product marketing, in [a statement](https://www.theverge.com/2022/1/13/22881211/microsoft-discontinues-xbox-one-consoles-2020) provided to *The Verge*. Until this confirmation, it was the understanding that all but the Xbox One S had ceased being manufactured. The original Xbox One was discontinued back in 2017, and both the Xbox One X and Xbox One S All-Digital Edition were confirmed to be discontinued in the middle of 2020. “As we ramp into the future with Xbox Series X, we’re taking the natural step of stopping production on Xbox One X and Xbox One S All-Digital Edition,” a statement at the time attributed to a Microsoft spokesperson reads in part. “Xbox One S will continue to be manufactured and sold globally.” It appears that sometime between July 2020 and December 2020, Microsoft stopped manufacturing even the regular Xbox One S. The discontinuation going largely unnoticed likely has something to do with both the ongoing COVID-19 pandemic disrupting normal life as well as the fact that Xbox has made it a priority to ensure new first-party titles can be played on just about any console from it. And that’s not even getting into the prevalence of the Xbox Game Pass subscription service. **Categories:** Games, News **Tags:** xbox --- ### [Copy to Clipboard component for Blazor](https://puresourcecode.com/dotnet/blazor/copy-to-clipboard-component-for-blazor/) **Published:** January 13, 2022 **Author:** Enrico **Excerpt:** I'll create a Copy to Clipboard component for Blazor. I use the button to notify if the copy is successful. So, I return reset the button **Content:** In this new post, I will create a Copy to Clipboard component for Blazor. I use the button to notify if the copy is successful. Then, I return the button to its original state. Here’s how the app looks when it works correctly. ![The behaviour of the component when it can copy the text in the clipboard- Copy to Clipboard component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/149396754-e2014f5e-a982-4688-86b6-772e8c62c33c.gif?w=640&ssl=1)The behaviour of the component when it can copy the text in the clipboard And here’s how it looks when the copy fails. ![ The behaviour of the component when it can't copy the text in the clipboard - Copy to Clipboard component for Blazor](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/149396769-e54774e5-83ce-45ae-89aa-dd9847172a77.gif?w=640&ssl=1) The behaviour of the component when it can’t copy the text in the clipboard We’ll build a component that allows users to copy and paste text from a markdown previewer. This process involves three steps: - Implement a `ClipboardService` - Create a shared `CopyToClipboardButton` component - Use the component with a markdown previewer The ful source code is available on [GitHub](https://github.com/erossini/BlazorCopyToClipboard).. ## Implement a ClipboardService So, to write text to the clipboard, we’ll need to use a browser API. This work involves some quick *JavaScript*, whether from a pre-built component or some JavaScript interoperability. Luckily for us, we can create a basic `ClipboardService` that allows us to use `IJsRuntime` to call the [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API), which [is widely used](https://caniuse.com/mdn-api_clipboard_writetext) in today’s browsers. Then, we’ll create a `WriteTextAsync` method that takes in the text to copy. Then, we’ll write the text to the API with a *navigator.clipboard.writeText* call. Here’s the code for `Services/ClipboardService.cs`: ``` using Microsoft.JSInterop; namespace PSC.Blazor.Components.CopyToClipboard { public class ClipboardService { private readonly Lazy moduleTask; private readonly IJSRuntime _jsRuntime; public ClipboardService(IJSRuntime jsRuntime) { _jsRuntime = jsRuntime; } public ValueTask WriteTextAsync(string text) { return _jsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); } } } ``` Then, in `Program.cs`, reference the new service we created: ``` builder.Services.AddScoped(); ``` With that out of the way, let’s create the `CopyToClipboardButton` component. ## Create a shared CopyToClipboardButton component So, at the top of the file, let’s inject our `ClipboardService`. (We won’t need a `@page` directive since this will be a shared component and not a routable page.) ``` @inject ClipboardService ClipboardService ``` Now, we’ll need to understand how the button will look. For both the active and notification states, we need to have the following: - Message to display - Font Awesome icon to display - Bootstrap button class With that in mind, let’s define all those at the beginning of the component’s `@code` block. ``` @code { [Parameter] public string Id { get; set; } = "CopyToClipboard-" + Guid.NewGuid().ToString(); [Parameter] public string SuccessButtonClass { get; set; } = "btn btn-success"; [Parameter] public string InfoButtonClass { get; set; } = "btn btn-info"; [Parameter] public string ErrorButtonClass { get; set; } = "btn btn-danger"; [Parameter] public string CopyToClipboardText { get; set; } = "Copy to clipboard"; [Parameter] public string CopiedToClipboardText { get; set; } = "Copied to clipboard!"; [Parameter] public string ErrorText { get; set; } = "Oops. Try again."; [Parameter] public string FontAwesomeCopyClass { get; set; } = "fa fa-clipboard"; [Parameter] public string FontAwesomeCopiedClass { get; set; } = "fa fa-check"; [Parameter] public string FontAwesomeErrorClass { get; set; } = "fa fa-exclamation-circle"; [Parameter] public string Text { get; set; } } ``` With that, we need to include a `Text` property as a component parameter. The caller will provide this to us, so we know what to copy. ``` [Parameter] public string Text { get; set; } ``` Now, using for C# 9 `records` and `target` typing, we can create an immutable object to work with the initial state. ``` record ButtonData(bool IsDisabled, string ButtonText, string ButtonClass, string FontAwesomeClass); ButtonData buttonData = new(false, CopyToClipboardText, InfoButtonClass, FontAwesomeCopyClass); ``` Now, in the markup, we can add a new button with the properties we defined. ``` @buttonData.ButtonText ``` You’ll get an error because your editor doesn’t know about the `CopyToClipboard` method. Let’s create it. First, set up an `originalData` variable that holds the original state, so we have it when it changes. ``` var originalData = buttonData; ``` Now, we’ll do the following in a try/catch block: - Write the text to the clipboard - Update `buttonData` to show it was a success/failure - Call `StateHasChanged` - Wait 1500 milliseconds - Return `buttonData` to its original state We need to explicitly call `StateHasChanged` to notify the component it needs to re-render because the state … has changed. Here’s the full `CopyToClipboard` method (along with a `TriggerButtonState` private method for reusability). ``` public async Task ToClipboard() { var originalData = buttonData; try { await ClipboardService.WriteTextAsync(Text); buttonData = new ButtonData(true, CopiedToClipboardText, SuccessButtonClass, FontAwesomeCopiedClass); await TriggerButtonState(); buttonData = originalData; } catch { buttonData = new ButtonData(true, ErrorText, ErrorButtonClass, FontAwesomeErrorClass); await TriggerButtonState(); buttonData = originalData; } } private async Task TriggerButtonState() { StateHasChanged(); await Task.Delay(TimeSpan.FromMilliseconds(1500)); } ``` For reference, here’s the entire `CopyToClipboardButton` component: ``` @inject ClipboardService ClipboardService @code { [Parameter] public string Id { get; set; } = "CopyToClipboard-" + Guid.NewGuid().ToString(); [Parameter] public string SuccessButtonClass { get; set; } = "btn btn-success"; [Parameter] public string InfoButtonClass { get; set; } = "btn btn-info"; [Parameter] public string ErrorButtonClass { get; set; } = "btn btn-danger"; [Parameter] public string CopyToClipboardText { get; set; } = "Copy to clipboard"; [Parameter] public string CopiedToClipboardText { get; set; } = "Copied to clipboard!"; [Parameter] public string ErrorText { get; set; } = "Oops. Try again."; [Parameter] public string FontAwesomeCopyClass { get; set; } = "fa fa-clipboard"; [Parameter] public string FontAwesomeCopiedClass { get; set; } = "fa fa-check"; [Parameter] public string FontAwesomeErrorClass { get; set; } = "fa fa-exclamation-circle"; [Parameter] public string Text { get; set; } record ButtonData(bool IsDisabled, string ButtonText, string ButtonClass, string FontAwesomeClass); ButtonData buttonData; protected override void OnInitialized() { buttonData = new(false, CopyToClipboardText, InfoButtonClass, FontAwesomeCopyClass); base.OnInitialized(); } public async Task ToClipboard() { var originalData = buttonData; try { await ClipboardService.WriteTextAsync(Text); buttonData = new ButtonData(true, CopiedToClipboardText, SuccessButtonClass, FontAwesomeCopiedClass); await TriggerButtonState(); buttonData = originalData; } catch { buttonData = new ButtonData(true, ErrorText, ErrorButtonClass, FontAwesomeErrorClass); await TriggerButtonState(); buttonData = originalData; } } private async Task TriggerButtonState() { StateHasChanged(); await Task.Delay(TimeSpan.FromMilliseconds(1500)); } } ``` Great! You should now be able to see the button in action. ## Use the component with a markdown previewer So, now we can build a page to use the component. First, install the component from NuGet and then add it in the `_Imports.razor` ``` @using PSC.Blazor.Components.CopyToClipboard ``` We can now build a simple with a simple `TextArea`. Now, the page contains the following code ``` @page "/"
@code { public string Body { get; set; } = string.Empty; } ``` So, I’m adding a `textarea`, binding to the `Body` text. That’s really all there is to it! ## Wrap up In this post, we built a reusable CopyToClipboard component for Blazor to copy text to the clipboard. As a bonus, the component toggles between active and notification states. **Categories:** .NET6, Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly, net6 --- ### [CSS not loading from App_Themes](https://puresourcecode.com/dotnet/asp-net/css-not-loading-from-app-themes/) **Published:** May 5, 2014 **Author:** Enrico **Excerpt:** am having problem while loading the CSS file through App_Themes folder but with ASP.NET authentication doesn't work. **Content:** In this post I show how to fix your ASP.NET application if the [CSS](https://en.wikipedia.org/wiki/CSS) not loading from App\_Themes. A theme is a collection of property settings that allow you to define the look of pages and controls, and then apply the look consistently across pages in a Web application, across an entire Web application, or across all Web applications on a server. A set of example ASP.NET themes is also available: [Download](https://go.microsoft.com/fwlink/?linkid=157239). ## [](https://docs.microsoft.com/en-us/previous-versions/aspnet/ykzx33wh(v=vs.100)#themes-and-control-skins)Themes and Control Skins Themes are made up of a set of elements: skins, cascading style sheets (CSS), images, and other resources. At a minimum, a theme will contain skins. Themes are defined in special directories in your Web site or on your Web server. ### [](https://docs.microsoft.com/en-us/previous-versions/aspnet/ykzx33wh(v=vs.100)#skins)Skins A skin file has the file name extension .skin and contains property settings for individual controls such as [Button](https://msdn.microsoft.com/en-us/library/3e83tsk6(v=vs.100)), [Label](https://msdn.microsoft.com/en-us/library/620f4ses(v=vs.100)), [TextBox](https://msdn.microsoft.com/en-us/library/4b1xz97b(v=vs.100)), or [Calendar](https://msdn.microsoft.com/en-us/library/15a20f24(v=vs.100)) controls. Control skin settings are like the control markup itself, but contain only the properties you want to set as part of the theme. For example, the following is a control skin for a [Button](https://msdn.microsoft.com/en-us/library/3e83tsk6(v=vs.100)) control. ## The problem I am having problem while loading the CSS file through `App_Themes` folder but with [ASP.NET](https://puresourcecode.com/category/dotnet/asp-net/) authentication doesn’t work. The solutions are two. First you have to insert in `web.config` this tag: ``` ``` The second solution (that it is always working) is changing tag page on `web.config`. ``` ``` ![ASP.NET - CSS not loading from App_Themes](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/aspnet.png?resize=640%2C320&ssl=1)ASP.NET ## More about ASP.NET - [Render In MVC A Link With Image And Text](https://puresourcecode.com/dotnet/asp-net/render-in-mvc-a-link-with-image-and-text/) - [Creating A URL Shortener Using ASP.NET WepAPI And MVC: Error Handling](https://puresourcecode.com/dotnet/asp-net/creating-a-url-shortener-using-asp-net-wepapi-and-mvc-error-handling/) - [Creating A URL Shortener Using ASP.NET WepAPI And MVC: Implementing The Business Layer](https://puresourcecode.com/dotnet/asp-net/creating-a-url-shortener-using-asp-net-wepapi-and-mvc-implementing-the-business-layer/) - [Creating A URL Shortener Using ASP.NET WepAPI And MVC](https://puresourcecode.com/dotnet/asp-net/creating-a-url-shortener-using-asp-net-wepapi-and-mvc/) **Categories:** .NET, ASP.NET **Tags:** aspnet, themes --- ### [A successful Git branching model](https://puresourcecode.com/tools/a-successful-git-branching-model/) **Published:** March 31, 2020 **Author:** Enrico **Content:** In this post I present the development model that I’ve introduced for some of my projects (both at work and private) about a year ago, and which has turned out to be very successful. I’ve been meaning to write about it for a while now, but I’ve never really found the time to do so thoroughly, until now. I won’t talk about any of the projects’ details, merely about the branching strategy and release management. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/03/git-model%402x.png?w=640&ssl=1) ## Why git? For a thorough discussion on the pros and cons of Git compared to centralized source code control systems, see the [web](https://git.or.cz/gitwiki/GitSvnComparsion). There are plenty of flame wars going on there. As a developer, I prefer Git above all other tools around today. Git really changed the way developers think of merging and branching. From the classic CVS/Subversion world I came from, merging/branching has always been considered a bit scary (“beware of merge conflicts, they bite you!”) and something you only do every once in a while. But with Git, these actions are extremely cheap and simple, and they are considered one of the core parts of your *daily* workflow, really. For example, in CVS/Subversion [books](https://svnbook.red-bean.com/), branching and merging is first discussed in the later chapters (for advanced users), while in [every](https://book.git-scm.com/) [Git](https://pragprog.com/titles/tsgit/pragmatic-version-control-using-git) [book](https://github.com/progit/progit), it’s already covered in chapter 3 (basics). As a consequence of its simplicity and repetitive nature, branching and merging are no longer something to be afraid of. Version control tools are supposed to assist in branching/merging more than anything else. Enough about the tools, let’s head onto the development model. The model that I’m going to present here is essentially no more than a set of procedures that every team member has to follow in order to come to a managed software development process. ## Decentralized but centralized The repository setup that we use and that works well with this branching model, is that with a central “truth” repo. Note that this repo is only *considered* to be the central one (since Git is a DVCS, there is no such thing as a central repo at a technical level). We will refer to this repo as `origin`, since this name is familiar to all Git users. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/03/centr-decentr%402x.png?w=640&ssl=1) Each developer pulls and pushes to origin. But besides the centralized push-pull relationships, each developer may also pull changes from other peers to form sub teams. For example, this might be useful to work together with two or more developers on a big new feature, before pushing the work in progress to `origin` prematurely. In the figure above, there are subteams of Alice and Bob, Alice and David, and Clair and David. Technically, this means nothing more than that Alice has defined a Git remote, named `bob`, pointing to Bob’s repository, and vice versa. ## The main branches At the core, the development model is greatly inspired by existing models out there. The central repo holds two main branches with an infinite lifetime: - `master` - `develop` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/03/main-branches%402x.png?resize=265%2C399&ssl=1) The `master` branch at `origin` should be familiar to every Git user. Parallel to the `master` branch, another branch exists called `develop`. We consider `origin/master` to be the main branch where the source code of `HEAD` always reflects a *production-ready* state. We consider `origin/develop` to be the main branch where the source code of `HEAD` always reflects a state with the latest delivered development changes for the next release. Some would call this the “integration branch”. This is where any automatic nightly builds are built from. When the source code in the `develop` branch reaches a stable point and is ready to be released, all of the changes should be merged back into `master` somehow and then tagged with a release number. How this is done in detail will be discussed further on. Therefore, each time when changes are merged back into `master`, this is a new production release *by definition*. We tend to be very strict at this, so that theoretically, we could use a Git hook script to automatically build and roll-out our software to our production servers everytime there was a commit on `master`. ## Supporting branches Next to the main branches `master` and `develop`, our development model uses a variety of supporting branches to aid parallel development between team members, ease tracking of features, prepare for production releases and to assist in quickly fixing live production problems. Unlike the main branches, these branches always have a limited life time, since they will be removed eventually. The different types of branches we may use are: - Feature branches - Release branches - Hotfix branches Each of these branches have a specific purpose and are bound to strict rules as to which branches may be their originating branch and which branches must be their merge targets. We will walk through them in a minute. By no means are these branches “special” from a technical perspective. The branch types are categorized by how we *use* them. They are of course plain old Git branches. ### Feature branches May branch off from:`develop`Must merge back into:`develop`Branch naming convention:anything except `master`, `develop`, `release-*`, or `hotfix-*` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/03/fb%402x.png?resize=156%2C419&ssl=1) Feature branches (or sometimes called topic branches) are used to develop new features for the upcoming or a distant future release. When starting development of a feature, the target release in which this feature will be incorporated may well be unknown at that point. The essence of a feature branch is that it exists as long as the feature is in development, but will eventually be merged back into `develop` (to definitely add the new feature to the upcoming release) or discarded (in case of a disappointing experiment). Feature branches typically exist in developer repos only, not in `origin`. #### Creating a feature branch When starting work on a new feature, branch off from the `develop` branch. ``` $ git checkout -b myfeature develop Switched to a new branch "myfeature" ``` #### Incorporating a finished feature on develop Finished features may be merged into the `develop` branch to definitely add them to the upcoming release: ``` $ git checkout develop Switched to branch 'develop' $ git merge --no-ff myfeature Updating ea1b82a..05e9557 (Summary of changes) $ git branch -d myfeature Deleted branch myfeature (was 05e9557). $ git push origin develop ``` The `--no-ff` flag causes the merge to always create a new commit object, even if the merge could be performed with a fast-forward. This avoids losing information about the historical existence of a feature branch and groups together all commits that together added the feature. Compare: ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/03/merge-without-ff%402x.png?w=640&ssl=1)In the latter case, it is impossible to see from the Git history which of the commit objects together have implemented a feature—you would have to manually read all the log messages. Reverting a whole feature (i.e. a group of commits), is a true headache in the latter situation, whereas it is easily done if the `--no-ff` flag was used. Yes, it will create a few more (empty) commit objects, but the gain is much bigger than the cost. ### Release branches May branch off from:`develop`Must merge back into:`develop` and `master`Branch naming convention:`release-*` Release branches support preparation of a new production release. They allow for last-minute dotting of i’s and crossing t’s. Furthermore, they allow for minor bug fixes and preparing meta-data for a release (version number, build dates, etc.). By doing all of this work on a release branch, the `develop` branch is cleared to receive features for the next big release. The key moment to branch off a new release branch from `develop` is when develop (almost) reflects the desired state of the new release. At least all features that are targeted for the release-to-be-built must be merged in to `develop` at this point in time. All features targeted at future releases may not—they must wait until after the release branch is branched off. It is exactly at the start of a release branch that the upcoming release gets assigned a version number—not any earlier. Up until that moment, the `develop` branch reflected changes for the “next release”, but it is unclear whether that “next release” will eventually become 0.3 or 1.0, until the release branch is started. That decision is made on the start of the release branch and is carried out by the project’s rules on version number bumping. #### Creating a release branch Release branches are created from the `develop` branch. For example, say version 1.1.5 is the current production release and we have a big release coming up. The state of `develop` is ready for the “next release” and we have decided that this will become version 1.2 (rather than 1.1.6 or 2.0). So we branch off and give the release branch a name reflecting the new version number: ``` $ git checkout -b release-1.2 develop Switched to a new branch "release-1.2" $ ./bump-version.sh 1.2 Files modified successfully, version bumped to 1.2. $ git commit -a -m "Bumped version number to 1.2" [release-1.2 74d9424] Bumped version number to 1.2 1 files changed, 1 insertions(+), 1 deletions(-) ``` After creating a new branch and switching to it, we bump the version number. Here, `bump-version.sh` is a fictional shell script that changes some files in the working copy to reflect the new version. (This can of course be a manual change—the point being that *some* files change.) Then, the bumped version number is committed. This new branch may exist there for a while, until the release may be rolled out definitely. During that time, bug fixes may be applied in this branch (rather than on the `develop` branch). Adding large new features here is strictly prohibited. They must be merged into `develop`, and therefore, wait for the next big release. #### Finishing a release branch When the state of the release branch is ready to become a real release, some actions need to be carried out. First, the release branch is merged into `master` (since every commit on `master` is a new release *by definition*, remember). Next, that commit on `master` must be tagged for easy future reference to this historical version. Finally, the changes made on the release branch need to be merged back into `develop`, so that future releases also contain these bug fixes. The first two steps in Git: ``` $ git checkout master Switched to branch 'master' $ git merge --no-ff release-1.2 Merge made by recursive. (Summary of changes) $ git tag -a 1.2 ``` The release is now done, and tagged for future reference. > **Edit:** You might as well want to use the `-s` or `-u ` flags to sign your tag cryptographically. To keep the changes made in the release branch, we need to merge those back into `develop`, though. In Git: ``` $ git checkout develop Switched to branch 'develop' $ git merge --no-ff release-1.2 Merge made by recursive. (Summary of changes) ``` This step may well lead to a merge conflict (probably even, since we have changed the version number). If so, fix it and commit. Now we are really done and the release branch may be removed, since we don’t need it anymore: ``` $ git branch -d release-1.2 Deleted branch release-1.2 (was ff452fe). ``` ### Hotfix branches May branch off from:`master`Must merge back into:`develop` and `master`Branch naming convention:`hotfix-*` ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/03/hotfix-branches%402x.png?resize=385%2C518&ssl=1) Hotfix branches are very much like release branches in that they are also meant to prepare for a new production release, albeit unplanned. They arise from the necessity to act immediately upon an undesired state of a live production version. When a critical bug in a production version must be resolved immediately, a hotfix branch may be branched off from the corresponding tag on the master branch that marks the production version. The essence is that work of team members (on the `develop` branch) can continue, while another person is preparing a quick production fix. #### Creating the hotfix branch Hotfix branches are created from the `master` branch. For example, say version 1.2 is the current production release running live and causing troubles due to a severe bug. But changes on `develop` are yet unstable. We may then branch off a hotfix branch and start fixing the problem: ``` $ git checkout -b hotfix-1.2.1 master Switched to a new branch "hotfix-1.2.1" $ ./bump-version.sh 1.2.1 Files modified successfully, version bumped to 1.2.1. $ git commit -a -m "Bumped version number to 1.2.1" [hotfix-1.2.1 41e61bb] Bumped version number to 1.2.1 1 files changed, 1 insertions(+), 1 deletions(-) ``` Don’t forget to bump the version number after branching off! Then, fix the bug and commit the fix in one or more separate commits. ``` $ git commit -m "Fixed severe production problem" [hotfix-1.2.1 abbe5d6] Fixed severe production problem 5 files changed, 32 insertions(+), 17 deletions(-) ``` #### Finishing a hotfix branch When finished, the bugfix needs to be merged back into `master`, but also needs to be merged back into `develop`, in order to safeguard that the bugfix is included in the next release as well. This is completely similar to how release branches are finished. First, update `master` and tag the release. ``` $ git checkout master Switched to branch 'master' $ git merge --no-ff hotfix-1.2.1 Merge made by recursive. (Summary of changes) $ git tag -a 1.2.1 ``` > **Edit:** You might as well want to use the `-s` or `-u ` flags to sign your tag cryptographically. Next, include the bugfix in `develop`, too: ``` $ git checkout develop Switched to branch 'develop' $ git merge --no-ff hotfix-1.2.1 Merge made by recursive. (Summary of changes) ``` The one exception to the rule here is that, **when a release branch currently exists, the hotfix changes need to be merged into that release branch, instead of `develop`**. Back-merging the bugfix into the release branch will eventually result in the bugfix being merged into `develop` too, when the release branch is finished. (If work in `develop` immediately requires this bugfix and cannot wait for the release branch to be finished, you may safely merge the bugfix into `develop` now already as well.) Finally, remove the temporary branch: ``` $ git branch -d hotfix-1.2.1 Deleted branch hotfix-1.2.1 (was abbe5d6). ``` If you want to know more about Gif Flow and Visual Studio, read me post about [digital transformation](https://puresourcecode.com/dotnet/digital-transformation-scenario-azure-visual-studio-git/). **Categories:** Tools **Tags:** branches, commit, git, git flow, hotfixes, release --- ### [Get the Most Out of iOS 11 with Visual Studio Tools for Xamarin](https://puresourcecode.com/dotnet/xamarin/get-the-most-out-of-ios-11-with-visual-studio-tools-for-xamarin/) **Published:** December 15, 2017 **Author:** Enrico **Content:** Join Craig Dunn explains what’s new in iOS 11 and how to take advantage of the latest updates – from drag-and-drop for iPad to machine learning and more – 100% in .NET and [Visual Studio](https://puresourcecode.com/dotnet/digital-transformation-scenario-azure-visual-studio-git/). Whether you’re building new or updating existing Xamarin.iOS apps, you’ll see how to implement new frameworks, APIs, and UI features, walk-through code samples, get expert tips and tricks, so you can start shipping iOS 11-ready apps to your users. In this webinar, you’ll: - Explore iOS 11 UI changes, including adapting to the iPhone X form factor - Dive into what .NET developers need to know about iOS 11 - Add iOS 11 features to new and existing Xamarin apps with step-by-step examples - Ensure backwards compatibility with prior OS versions - Learn how to incorporate Azure Machine Learning tools into CoreML - Ask questions and receive guidance from our team of app experts > To develop for iOS 11, you’ll need to have a machine that supports macOS Sierra and Xcode 9. Download the source code of this video from [GitHub](https://github.com/conceptdev/xamarin-ios-samples/tree/master/Todo11). **Categories:** Xamarin --- ### [Create a project for Azure Function in F# and Visual Studio 2019](https://puresourcecode.com/dotnet/fsharp/create-a-project-for-azure-function-in-f-and-visual-studio-2019/) **Published:** April 29, 2020 **Author:** Enrico **Excerpt:** How to create a solution and a project for Azure Functions in F# with Visual Studio 2019 **Content:** If you think creating a project for Azure Functions in Visual Studio 2019, it is easy, I have the bad news for you! Easy scenario. You want to create a simple Azure Function project with F# using Visual Studio 2019. Easy, right? First, launch Visual Studio 2019 and select “**Create a new project**“ ![Visual Studio 2019 Start page](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-13.png?resize=640%2C425&ssl=1)Visual Studio 2019 Start page Then, you can create new project with the wizard. For example, if you select from the dropdown “All languages” **F#** and from “All platforms” **Azure**, surprise surprise… ![Visual Studio 2019 - Create a new project](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-15.png?resize=640%2C425&ssl=1)Visual Studio 2019 – Create a new project You can’t! There is no option for that. My problem is that I have to create a solution for Azure Functions in F#. ![Visual Studio 2019 - Create a new project for Azure Function and F#](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-16.png?resize=640%2C425&ssl=1)Visual Studio 2019 – Create a new project for Azure Function and F# Depressing, I found a workaround that I want to share with you. Hopefully, you can give me a better solution. Create a new solution in **C#** for **Azure Functions**. ![Visual Studio 2019 and Azure Functions in C#](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-17.png?resize=640%2C425&ssl=1)Visual Studio 2019 and Azure Functions in C# Press **Next**. ![Visual Studio 2019: configure your new project](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-18.png?resize=640%2C425&ssl=1)Visual Studio 2019: configure your new project Select where you want to create your project and then press **Next**. ![Visual Studio 2019: create a new Azure Functions Application](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-19.png?resize=640%2C444&ssl=1)Visual Studio 2019: create a new Azure Functions Application Http Trigger is ok for this example; we have time to complicate the function later. Press **Create** and then you have you C# project in a solution! Run the project to be sure it is working fine. ![Azure Function up and running](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-22.png?resize=640%2C505&ssl=1)Azure Function up and running Done! Oh, wait, F#? This is the funny part. Follow my simple instructions: - close Visual Studio - rename `.csproj` to `.fsproj` - open the solution file and replace `.csproj` to `.fsproj` - edit the `.fsproj` file and make sure the following items are in there for Azure Functions v3: ``` netcoreapp3.1 v3 vspan_aBillity PreserveNewest PreserveNewest Never ``` Open this solution with Visual Studio again. Now, the solution is in F#. You can add a new Azure Function in F#. ![Visual Studio 2019: add new item](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-23.png?resize=640%2C444&ssl=1)Visual Studio 2019: add new item Replace the content of the new file with this one: ``` namespace FunctionApp1 open System open Microsoft.Azure.WebJobs open Microsoft.Azure.WebJobs.Host open System; open System.IO; open System.Threading.Tasks; open Microsoft.AspNetCore.Mvc; open Microsoft.Azure.WebJobs; open Microsoft.Azure.WebJobs.Extensions.Http; open Microsoft.AspNetCore.Http; open Microsoft.Extensions.Logging; module GetOrganizations = [] let Run ([] req: HttpRequest) (log: ILogger) = async { return "some result" } |> Async.StartAsTask ``` Play the project. The function is working and then you can browse your function! ![Azure Function in F# is working](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-25.png?resize=640%2C335&ssl=1)Azure Function in F# is working ![Call the function with your browser](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2020/04/image-24.png?resize=640%2C268&ssl=1)Call the function with your browser You find this code on my [Github](https://github.com/erossini/FSharpAzureFunctions). Also, if you want to know more about what Visual Studio can do for you, I recommend to read my article about [digital transformation](https://puresourcecode.com/dotnet/digital-transformation-scenario-azure-visual-studio-git/). **Categories:** Azure, F# **Tags:** azure-functions --- ### [Why Visual Studio Code?](https://puresourcecode.com/news/microsoft/why-visual-studio-code/) **Published:** April 29, 2015 **Author:** Enrico **Content:** ![](data:image/png;base64,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) [Visual Studio Code](https://code.visualstudio.com) provides developers with a new choice of developer tool that combines the simplicity and streamlined experience of a code editor with the best of what developers need for their core code-edit-debug cycle. Visual Studio Code is the first code editor, and first cross-platform development tool – supporting OSX, Linux, and Windows – in the Visual Studio family. At its heart, Visual Studio Code features a powerful, fast code editor great for day-to-day use. The Preview release of Code already has many of the features developers need in a code and text editor, including navigation, keyboard support with customizable bindings, syntax highlighting, bracket matching, auto indentation, and snippets, with support for dozens of languages. For serious coding, developers often need to work with code as more than just text. Visual Studio Code includes built-in support for always-on IntelliSense code completion, richer semantic code understanding and navigation, and code refactoring. In the Preview, Code includes enriched built-in support for ASP.NET 5 development with C#, and Node.js development with TypeScript and JavaScript, powered by the same underlying technologies that drive Visual Studio. Code includes great tooling for web technologies such as HTML, CSS, LESS, SASS, and JSON. Code also integrates with package managers and repositories, and builds and other common tasks to make everyday workflows faster. And Code understands Git, and delivers great Git workflows and source diffs integrated with the editor. But developers don’t spend all their time just writing code: they go back and forth between coding and debugging. Debugging is the most popular feature in Visual Studio, and often the one feature from an IDE that developers want in a leaner coding experience. Visual Studio Code includes a streamlined, integrated debugging experience, with support for Node.js debugging in the Preview, and more to come later. Architecturally, Visual Studio Code combines the best of web, native, and language-specific technologies. Using the [GitHub Electron Shell](https://github.com/atom/electron), Code combines web technologies such as JavaScript and Node.js with the speed and flexibility of native apps. Code uses a newer, faster version of the same industrial-strength HTML-based editor that has powered the “Monaco” cloud editor, Internet Explorer’s F12 Tools, and other projects. And Code uses a tools service architecture that enables it to use many of the same technologies that power Visual Studio, including Roslyn for .NET, TypeScript, the Visual Studio debugging engine, and more. In future previews, as we continue to evolve and refine this architecture, Visual Studio Code will include a public extensibility model that lets developers build and use plug-ins, and richly customize their edit-build-debug experience. We are, of course, still very early with Visual Studio Code. If you prefer a code editor-centric development tool, or are building cross-platform web and cloud applications, we invite you to try out the Visual Studio Code Preview, and let us know what you think! ## Next Steps Read on to find out about: - [Code Basics](https://code.visualstudio.com/docs/codebasics) – a quick orientation of VSCode - [Editing Evolved](https://code.visualstudio.com/docs/editingevolved) – from code colorization & multi-cursor to IntelliSense - [Debugging](https://code.visualstudio.com/docs/debugging) – OK time for the really fun stuff – break, step, watch - [How to use Git in Visual Studio](https://puresourcecode.com/dotnet/digital-transformation-scenario-azure-visual-studio-git/) **Categories:** Microsoft --- ### [Markdown Editor component for Blazor](https://puresourcecode.com/dotnet/blazor/markdown-editor-component-for-blazor/) **Published:** January 11, 2022 **Author:** Enrico **Excerpt:** I have created a new Markdown Editor component flexible and rich of functionalities for Blazor WebAssembly and Blazor Server with .NET6. **Content:** Few weeks ago, I created a package for a [Markdown Editor component](https://puresourcecode.com/dotnet/blazor/markdown-editor-with-blazor/) for Blazor with very basic functionalities. After a couple of months of work, finally, I created a very nice Markdown Editor component based on [EasyMDE](https://easy-markdown-editor.tk/) flexible and rich of functionalities for [Blazor WebAssembly](https://puresourcecode.com/tag/blazor-webassembly/) and [Blazor Server](https://puresourcecode.com/tag/blazor-server/) with .NET6. ![Markdown Editor component for Blazor in action](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/148641050-653f6101-7099-4d76-9a59-45a44e32a275.gif?w=640&ssl=1) Markdown Editor component for Blazor in action The full source code of the component is available on [GitHub](https://github.com/erossini/BlazorMarkdownEditor). In the same repository, there is a demo project and another project for the integration between the Markdown Editor for Blazor and the API for uploading images. ## How to use First, in your Blazor project, add the NuGet package called [PSC.Blazor.Components.MarkdownEditor](https://www.nuget.org/packages/PSC.Blazor.Components.MarkdownEditor/). Then, add the Markdown Editor to your `_Imports.razor` ``` @using PSC.Blazor.Components.MarkdownEditor @using PSC.Blazor.Components.MarkdownEditor.EventsArgs ``` Now, in your `index.html` or `host.html` add those lines: ``` ``` Remember that `jQuery` is also required. The component contains the [EasyMDE](https://easy-markdown-editor.tk/) script version 2.15.0. Obviously, you can add this script in your project but if you use the script in the component, you are sure it works fine and all functionalities are tested. ### Add MarkdownEditor in a page Now, in a `Razor` page, we can add the component with these lines ``` Result @((MarkupString)markdownHtml) @code { string markdownValue = "#Markdown Editor\nThis is a test"; string markdownHtml; protected override void OnInitialized() { markdownHtml = Markdig.Markdown.ToHtml(markdownValue ?? string.Empty); base.OnInitialized(); } Task OnMarkdownValueChanged(string value) { return Task.CompletedTask; } Task OnMarkdownValueHTMLChanged(string value) { markdownHtml = value; return Task.CompletedTask; } } ``` So, the result is a nice Markdown Editor like in the following screenshot. This is a screenshot from the demo in this repository. ![Markdown Editor component for Blazor in action ](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/148641050-653f6101-7099-4d76-9a59-45a44e32a275.gif?w=640&ssl=1) Markdown Editor component for Blazor in action ### Code explained So, the initial text for the Markdown Editor is coming from the `markdownValue` that is a string containing Markdown text. For every change in the editor, the `Value` changes and the component raises an event, in this case `OnMarkdownValueChanged`: this event receives the Markdown text as a `value`. From here, I can process with my procedure the Markdown text to obtain the HTML but I want to simplify this step. So, the component converts the Markdown text in a HTML and raises the event `ValueHTMLChanged`. ## Documentation The Markdown Editor for Blazor has an estensive collection of properties to map all the functionalities in the JavaScript version. In this repository, there are 2 projects: - **MarkdownEditorDemo** is a Blazor Web Assembly project that contains 2 pages: `Index.razor` where I show how to use the component with the basic functions and `Upload.razor` that shows how to cope with the image upload. To test the upload, the project `MarkdownEditorDemo.Api` must run - **MarkdownEditorDemo.Api** this is an ASP.NET Core WebApi (.NET6) how to implement a proper API for uploading images. For more details, I wrote a post about [Uploading image with .NET](https://puresourcecode.com/dotnet/net6/upload-download-files-using-httpclient/). ### Properties NameDescriptionTypeDefaultAutoSaveEnabledGets or sets the setting for the auto save. Saves the text that’s being written and will load it back in the future. It will forget the text when the form it’s contained in is submitted. Recommended to choose a unique ID for the Markdown Editor.boolfalseAutoSaveIdGets or sets the automatic save identifier. You must set a unique string identifier so the component can autosave. Something that separates this from other instances of the component elsewhere on your website.stringDefault valueAutoSaveDelayDelay between saves, in milliseconds. Defaults to 10000 (10s).int10000 (10s)AutoSaveSubmitDelayDelay before assuming that submit of the form failed and saving the text, in milliseconds.int5000 (5s)AutoSaveTextText for autosavestringAutosaved:AutoSaveTimeFormatLocaleSet the format for the datetime to display. For more info, see the JavaScript documentation [DateTimeFormat instances](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)stringen-USAutoSaveTimeFormatYearSet the format for the yearstringnumericAutoSaveTimeFormatMonthSet the format for the monthstringlongAutoSaveTimeFormatDaySet the format for the daystring2-digitAutoSaveTimeFormatHourSet the format for the hourstring2-digitAutoSaveTimeFormatMinuteSet the format for the minutestring2-digitAutoDownloadFontAwesomeIf set to true, force downloads Font Awesome (used for icons). If set to false, prevents downloading.bool?nullCustomButtonClickedOccurs after the custom toolbar button is clicked.EventCallbackDirectionrtl or ltr. Changes text direction to support right-to-left languages. Defaults to ltr.stringltrErrorMessagesErrors displayed to the user, using the errorCallback option, where *image\_name*, *image\_size* and *image\_max\_size* will be replaced by their respective values, that can be used for customization or internationalization.MarkdownErrorMessagesHideIconsAn array of icon names to hide. Can be used to hide specific icons shown by default without completely customizing the toolbar.string\[\]‘side-by-side’, ‘fullscreen’ImageAcceptA comma-separated list of mime-types used to check image type before upload (note: never trust client, always check file types at server-side). Defaults to image/png, image/jpeg, image/jpg, image.gif.stringimage/png, image/jpeg, image/jpg, image.gifImageCSRFTokenCSRF token to include with AJAX call to upload image. For instance, used with Django backend.stringImageMaxSizeMaximum image size in bytes, checked before upload (note: never trust client, always check image size at server-side). Defaults to 1024 \* 1024 \* 2 (2Mb).long1024 \* 1024 \* 2 (2Mb)ImagePathAbsoluteIf set to true, will treat *imageUrl* from *imageUploadFunction* and *filePath* returned from *imageUploadEndpoint* as an absolute rather than relative path, i.e. not prepend window.location.origin to it.stringImageTextsTexts displayed to the user (mainly on the status bar) for the import image feature, where *image\_name*, *image\_size* and *image\_max\_size* will be replaced by their respective values, that can be used for customization or internationalization.MarkdownImageTextsnullImageUploadAuthenticationSchemaIf an authentication for the API is required, assign to this property the schema to use. `Bearer` is the common one.stringemptyImageUploadAuthenticationTokenIf an authentication for the API is required, assign to this property the tokenstringemptyLineNumbersIf set to true, enables line numbers in the editor.boolfalseLineWrappingIf set to false, disable line wrapping. Defaults to true.boolfalseMaxHeightSets fixed height for the composition area. minHeight option will be ignored. Should be a string containing a valid CSS value like “500px”. Defaults to undefined.stringMaxUploadImageMessageSizeGets or sets the max message size when uploading the file.long20 \* 1024MinHeightSets the minimum height for the composition area, before it starts auto-growing. Should be a string containing a valid CSS value like “500px”. Defaults to “300px”.string300pxPlaceholderIf set, displays a custom placeholder message.stringnullSegmentFetchTimeoutGets or sets the Segment Fetch Timeout when uploading the file.TimeSpan1 minShowIconsAn array of icon names to show. Can be used to show specific icons hidden by default without completely customizing the toolbar.string\[\]‘code’, ‘table’TabSizeIf set, customize the tab size. Defaults to 2.int2ThemeOverride the theme. Defaults to easymde.stringeasymdeToolbar\[Optional\] Gets or sets the content of the toolbar.RenderFragmentToolbarTipsIf set to false, disable toolbar button tips. Defaults to true.booltrueUploadImageIf set to true, enables the image upload functionality, which can be triggered by drag-drop, copy-paste and through the browse-file window (opened when the user clicks on the upload-image icon). Defaults to false.boolfalseValueGets or sets the markdown value.stringnullValueHTMLGets the HTML from the markdown value.stringnull### Events NameDescriptionTypeErrorCallbackA callback function used to define how to display an error message. Defaults to (errorMessage) => alert(errorMessage).FuncImageUploadChangedOccurs every time the selected image has changed.FuncImageUploadEndedOccurs when an individual image upload has ended.FuncImageUploadEndpointThe endpoint where the images data will be sent, via an asynchronous POST request. The server is supposed to save this image, and return a json response.stringImageUploadProgressedNotifies the progress of image being written to the destination stream.FuncImageUploadStartedOccurs when an individual image upload has started.FuncValueChangedAn event that occurs after the markdown value has changed.EventCallbackValueHTMLChangedAn event that occurs after the markdown value has changed and the new HTML code is available.EventCallback## Upload file Now, the Markdown Editor for Blazor can take care of uploading a file and add the relative Markdown code in the editor. For that, the property `UploadImage` has to set to `true`. Also, the upload API must be specified in the property `ImageUploadEndpoint`. In some cases, the API requires an authentication. The properties `ImageUploadAuthenticationSchema` and `ImageUploadAuthenticationToken` allow you to pass the correct schema and token to use in the call. Those values will be added to the `HttpClient` `POST` request in the header. Only if both properties are not null, they will be added to the header. So, the result is quite nice and you can see the Markdown Editor for Blazor in action in the following screenshot. How you can see, I drag an image on the editor and immediately the upload process starts. When the API returns then URL for the image, the editor adds a new Markdown text for the image. ![Markdown Editor component for Blazor Upload example](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/148955032-1d3dc558-f308-4134-b3fd-6d43a0e4e37a.gif?w=640&ssl=1) Markdown Editor component for Blazor Upload example ### How to create the API Now, if you want to allow users to upload pictures via the Markdown Editor for Blazor, the property `UploadImage` must set to `true`. Then, the endpoint for the API must be declared via the property `ImageUploadEndpoint`. The Markdown Editor has JavaScript under the cover and this can cause same complications. For this reason, I managed in the JavaScript code to have the uploaded imaged encoded in `Base64`. Then, a C# function takes care to upload the file to the specified API. This API must return a `200 HTTP code` and, as a content, the URL of the uploaded image. It seems easy but I took a lot of time to figure out how to do it. So, I created the post [Upload/Download Files Using HttpClient](https://puresourcecode.com/dotnet/net6/upload-download-files-using-httpclient/) to explain in details how to create the required API. The full source code of the component is available on [GitHub](https://github.com/erossini/BlazorMarkdownEditor). ## Toolbar icons Below are the built-in toolbar icons (only some of which are enabled by default), which can be reorganized however you like. “Name” is the name of the icon, referenced in the JS. “Action” is either a function or a URL to open. “Class” is the class given to the icon. “Tooltip” is the small tooltip that appears via the `title=""` attribute. Note that shortcut hints are added automatically and reflect the specified action if it has a key bind assigned to it (i.e. with the value of `action` set to `bold` and that of `tooltip` set to `Bold`, the final text the user will see would be “Bold (Ctrl-B)”). Additionally, you can add a separator between any icons by adding `"|"` to the toolbar array. NameActionTooltip ClassboldtoggleBoldBold fa fa-bolditalictoggleItalicItalic fa fa-italicstrikethroughtoggleStrikethroughStrikethrough fa fa-strikethroughheadingtoggleHeadingSmallerHeading fa fa-headerheading-smallertoggleHeadingSmallerSmaller Heading fa fa-headerheading-biggertoggleHeadingBiggerBigger Heading fa fa-lg fa-headerheading-1toggleHeading1Big Heading fa fa-header header-1heading-2toggleHeading2Medium Heading fa fa-header header-2heading-3toggleHeading3Small Heading fa fa-header header-3codetoggleCodeBlockCode fa fa-codequotetoggleBlockquoteQuote fa fa-quote-leftunordered-listtoggleUnorderedListGeneric List fa fa-list-ulordered-listtoggleOrderedListNumbered List fa fa-list-olclean-blockcleanBlockClean block fa fa-eraserlinkdrawLinkCreate Link fa fa-linkimagedrawImageInsert Image fa fa-picture-otabledrawTableInsert Table fa fa-tablehorizontal-ruledrawHorizontalRuleInsert Horizontal Line fa fa-minuspreviewtogglePreviewToggle Preview fa fa-eye no-disableside-by-sidetoggleSideBySideToggle Side by Side fa fa-columns no-disable no-mobilefullscreentoggleFullScreenToggle Fullscreen fa fa-arrows-alt no-disable no-mobileguide[This link](https://www.markdownguide.org/basic-syntax/)Markdown Guide fa fa-question-circle## Keyboard shortcuts The Markdown Editor component for Blazor comes with an array of predefined keyboard shortcuts, but they can be altered with a configuration option. The list of default ones is as follows: Shortcut (Windows / Linux)Shortcut (macOS)Action*Ctrl-‘**Cmd-‘*“toggleBlockquote”*Ctrl-B**Cmd-B*“toggleBold”*Ctrl-E**Cmd-E*“cleanBlock”*Ctrl-H**Cmd-H*“toggleHeadingSmaller”*Ctrl-I**Cmd-I*“toggleItalic”*Ctrl-K**Cmd-K*“drawLink”*Ctrl-L**Cmd-L*“toggleUnorderedList”*Ctrl-P**Cmd-P*“togglePreview”*Ctrl-Alt-C**Cmd-Alt-C*“toggleCodeBlock”*Ctrl-Alt-I**Cmd-Alt-I*“drawImage”*Ctrl-Alt-L**Cmd-Alt-L*“toggleOrderedList”*Shift-Ctrl-H**Shift-Cmd-H*“toggleHeadingBigger”*F9**F9*“toggleSideBySide”*F11**F11*“toggleFullScreen”## Wrap up In conclusion, this is a new Markdown Editor for Blazor, with a lot of functionalities, flexible and with image upload. I hope you like it and will use it. It you need help or for suggestion, please comment below or in the [Forum](https://puresourcecode.com/forum/). ## Other Blazor components - [DataTable for Blazor](https://puresourcecode.com/dotnet/net-core/datatable-component-for-blazor/): DataTable component for Blazor WebAssembly and Blazor Server - [Markdown editor for Blazor](https://puresourcecode.com/dotnet/blazor/markdown-editor-with-blazor/): This is a Markdown Editor for use in Blazor. It contains a live preview as well as an embedded help guide for users. - [Modal dialog for Blazor](https://puresourcecode.com/dotnet/blazor/modal-dialog-component-for-blazor/): Simple Modal Dialog for Blazor WebAssembly - [PSC.Extensions](https://puresourcecode.com/dotnet/net-core/a-lot-of-functions-for-net5/): A lot of functions for .NET6 in a NuGet package that you can download for free. We collected in this package functions for everyday work to help you with claim, strings, enums, date and time, expressions… - [Quill for Blazor](https://puresourcecode.com/dotnet/blazor/create-a-blazor-component-for-quill/): Quill Component is a custom reusable control that allows us to easily consume Quill and place multiple instances of it on a single page in our Blazor application - [Segment for Blazor](https://puresourcecode.com/dotnet/blazor/segment-control-for-blazor/): This is a Segment component for Blazor Web Assembly and Blazor Server - [Tabs for Blazor](https://puresourcecode.com/dotnet/blazor/tabs-control-for-blazor/): This is a Tabs component for Blazor Web Assembly and Blazor Server ## More examples and documentation - [Write a reusable Blazor component](https://puresourcecode.com/dotnet/blazor/write-a-reusable-blazor-component/) - [Getting Started With C# And Blazor](https://puresourcecode.com/dotnet/net-core/getting-started-with-c-and-blazor/) - [Setting Up A Blazor WebAssembly Application](https://puresourcecode.com/dotnet/blazor/setting-up-a-blazor-webassembly-application/) - [Working With Blazor Component Model](https://puresourcecode.com/dotnet/blazor/working-with-blazors-component-model/) - [Secure Blazor WebAssembly With IdentityServer4](https://puresourcecode.com/dotnet/blazor/secure-blazor-webassembly-with-identityserver4/) - [Blazor Using HttpClient With Authentication](https://puresourcecode.com/dotnet/blazor/blazor-using-httpclient-with-authentication/) - [InputSelect component for enumerations in Blazor](https://puresourcecode.com/dotnet/blazor/inputselect-component-for-enumerations-in-blazor/) - [Use LocalStorage with Blazor WebAssembly](https://puresourcecode.com/dotnet/blazor/use-localstorage-with-blazor-webassembly/) - [Modal Dialog component for Blazor](https://puresourcecode.com/dotnet/blazor/modal-dialog-component-for-blazor/) - [Create Tooltip component for Blazor](https://puresourcecode.com/dotnet/blazor/create-tooltip-component-for-blazor/) - [Consume ASP.NET Core Razor components from Razor class libraries | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/components/class-libraries?view=aspnetcore-5.0&tabs=visual-studio) **Categories:** .NET6, Blazor **Tags:** blazor, blazor-component, blazor-server, blazor-webassembly, markdown --- ### [Upload/Download Files using HttpClient](https://puresourcecode.com/dotnet/net-core/upload-download-files-using-httpclient/) **Published:** January 10, 2022 **Author:** Enrico **Content:** In this new post, I show you how to upload/download files using HttpClient in [C#](https://puresourcecode.com/tag/csharp/) and [.NET Core](https://puresourcecode.com/category/dotnet/net-core/). Creating a new version of the [Markdown Editor component](https://puresourcecode.com/dotnet/blazor/markdown-editor-with-blazor/) for [Blazor](https://puresourcecode.com/category/dotnet/blazor/), I face some issues with the file upload. So, I was working to find a solution and now I can tell you how to do it. First, I will take a look at how to send multipart MIME data to a [Web API](https://puresourcecode.com/category/dotnet/webapi/) using HttpClient. We will create two applications to demonstrate the data transfer between the *client side* and the *server side*. The server-side app is an ASP.NET Core web project, which includes a `Web API controller` for uploading and downloading files. The client-side app is a `Console project`, which contains a [Typed HttpClient](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-3.1#typed-clients) to send HTTP requests for file uploading and/or downloading. When an application needs to talk to another system, it is quite common that the application sends data to and receives data from the other system using `HttpClient` in the back-end. Based on [this article](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) in Microsoft Docs, it is straightforward to send HTTP requests and receive HTTP responses. However, most of tutorials and blog posts don’t talk much about sending `FormData` with a file object and a collection of key/value pairs using `HttpClient`. This blog post intends to provide the missing guide. The source code of this post is on [GitHub](https://github.com/erossini/HttpClientMultipart). Please leave your comment at the end of this post or in the [forum](https://puresourcecode.com/forum/). ## Web API for Uploading a File with FormData This API action method follows the example in [an article](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads) in Microsoft Docs. The implementation is lengthy, but the code logic demonstrates several checks to meet the security criteria. First, I’m going to create a Web API project with .NET6 and I create the controller `ImageController`. So, I create a `HttpPost` function for `Upload`. ``` [HttpPost] [DisableFormValueModelBinding] public async Task Upload() { if (!Request.ContentType.IsMultipartContentType()) { ModelState.AddModelError("File", "The request couldn't be processed (Error 1)."); _logger.LogWarning($"The request content type [{Request.ContentType}] is invalid."); return BadRequest(ModelState); } var formModel = new CustomFormModel(); var boundary = MediaTypeHeaderValue.Parse(Request.ContentType).GetBoundary( new FormOptions().MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, HttpContext.Request.Body); var section = await reader.ReadNextSectionAsync(); string trustedFileNameForFileStorage = String.Empty; while (section != null) { var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse( section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { if (contentDisposition.IsFileDisposition()) { // Don't trust the file name sent by the client. // To display the file name, HTML-encode the value. var trustedFileNameForDisplay = WebUtility.HtmlEncode(contentDisposition.FileName.Value); trustedFileNameForFileStorage = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()) + Path.GetExtension(trustedFileNameForDisplay); var streamedFileContent = await FileHelpers.ProcessStreamedFile(section, contentDisposition, ModelState, _permittedExtensions, _fileSizeLimit); if (!ModelState.IsValid) return BadRequest(ModelState); var trustedFilePath = Path.Combine(_targetFolderPath, trustedFileNameForFileStorage); using (var targetStream = System.IO.File.Create(trustedFilePath)) { await targetStream.WriteAsync(streamedFileContent); formModel.TrustedFilePath = trustedFilePath; formModel.TrustedFileName = trustedFileNameForDisplay; _logger.LogInformation($"Uploaded file '{trustedFileNameForDisplay}'" + $" saved to '{_targetFolderPath}' " + $"as {trustedFileNameForFileStorage}"); } } else if (contentDisposition.IsFormDisposition()) { var content = new StreamReader(section.Body).ReadToEnd(); if (contentDisposition.Name == "userId" && int.TryParse(content, out var useId)) formModel.UserId = useId; if (contentDisposition.Name == "comment") formModel.Comment = content; if (contentDisposition.Name == "isPrimary" && bool.TryParse(content, out var isPrimary)) formModel.IsPrimary = isPrimary; } } // Drain any remaining section body that hasn't been consumed and // read the headers for the next section. section = await reader.ReadNextSectionAsync(); } if (!string.IsNullOrEmpty(trustedFileNameForFileStorage)) { string host = $"{_httpContextAccessor.HttpContext.Request.Scheme}://" + $"{_httpContextAccessor.HttpContext.Request.Host.Value}"; int index = FileHelpers.GetExtensionId(Path.GetExtension(trustedFileNameForFileStorage)); return Ok($"{host}/api/files/{index}/" + $"{Path.GetFileNameWithoutExtension(trustedFileNameForFileStorage)}"); } else return BadRequest("It wasn't possible to upload the file"); } ``` ### Code explained I know, it is a lot of code. First, I check if the request has a multipart and if not, I raise an error. To work on the multipart, I created a helper. `MultipartRequestHelper` has 2 functions: - `IsMultipartContentType`: check the content type and verify if the is a multipart - `GetBoundary`: check the `Boundary` in the `ContentType` and it don’t exceed the length limit Then, I create a new instance of `CustomFormModel`: this class is only for demo purpose to save some data from the POST request such as the user Id, a comment or a Boolean value. I don’t use this data but it is interesting to understand how to read them. In the client side, I will show you how to send this information. Now, the procedure is starting to read each section of the multipart request. The section could contain a file or form data. If there is a file section, I’m going to read the file and return a `byte[]` (array of bytes) using the `FileHelpers`. If it is a form section, I read its body and try to match the name with a known variable. At the end, if the file is saved on the file system and then `trustedFileNameForFileStorage` is not null, the API returns the full URL of the new uploaded image. To obtain the base URL of the API, I use `HttpContextAccessor`. So, I have the `Scheme` of the API (fo example `HTTPS`) and the host (for example `localhost:4100`). For using the `HttpContextAccessor` remember to add in the `Startup.cs` the dependency ``` services.AddSingleton(); ``` Then, I like to have a nice URL to share and for this reason I replace the extension with a number and then the new name of the file like that ``` https://localhost:44391/api/files/1/ys45k0ai ``` Maybe this URL is not very user-friendly, so, I like to move the `Download` function in the `HomeController`. So, the resulted URL is quite easy to read and write ``` https://localhost:44391/1/ys45k0ai ``` If you are thinking why I want to have a nice URL, the answer is easy. I’m working on the new version of the [Markdown Editor for Blazor](https://puresourcecode.com/dotnet/blazor/markdown-editor-with-blazor/). So, I want to have a functionality to upload file and display a nice URL. I’ll keep you update about it. ### MultipartRequestExtensions ``` public static class MultipartRequestExtensions { // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" // The spec at https://tools.ietf.org/html/rfc2046#section-5.1 states that 70 characters // is a reasonable limit. public static string GetBoundary(this MediaTypeHeaderValue contentType, int lengthLimit) { var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; if (string.IsNullOrWhiteSpace(boundary)) throw new InvalidDataException("Missing content-type boundary."); if (boundary.Length > lengthLimit) throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); return boundary; } public static bool IsMultipartContentType(this string contentType) { return !string.IsNullOrEmpty(contentType) && contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0; } } ``` ## Send Multipart FormData using HttpClient So, we need to use an `HTTP` `POST` method to send content to a server-side resource. The tricky part is constructing the `HTTP` request body content because we need to combine the file data and a collection of key/value pairs in one `FormData` object. The following code snippet shows an example solution. ``` public async Task UploadFile(string filePath) { _logger.LogInformation($"Uploading a text file [{filePath}]."); if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentNullException(nameof(filePath)); if (!File.Exists(filePath)) throw new FileNotFoundException($"File [{filePath}] not found."); using var form = new MultipartFormDataContent(); using var fileContent = new ByteArrayContent(await File.ReadAllBytesAsync(filePath)); fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse("multipart/form-data"); form.Add(fileContent, "file", Path.GetFileName(filePath)); form.Add(new StringContent("enrico"), "userId"); form.Add(new StringContent("this is a comments"), "comment"); form.Add(new StringContent("true"), "isPrimary"); var response = await _httpClient.PostAsync($"{_url}/api/files", form); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); _logger.LogInformation("Uploading is complete."); _logger.LogInformation($"API Response: {result}\n\n"); return result; } ``` So, the method `UploadFile(string filePath)` first validates the physical file. Then line 10 instantiates a `MultipartFormDataContent` object, which is the request content sent to the server-side app. Then, lines 11 and 11 create a `ByteArrayContent` object from the file content, and sets the `ContentType` header to be `“multipart/form-data”`. **Note:** When a file is included in a form, the `enctype` attribute should always be `“multipart/form-data”`, which specifies that the form will be sent as a multipart MIME message. If the `ContentType` is not set, then it will default to be `application/json`, which is not what we want here. Line 14 adds the file content to the form object, and sets the key to be `“file”`. The key can be different when multiple files are included in a form. Lines 15 to 15 are examples of adding key/value pairs to the `MultipartFormDataContent` object. The values can only be represented as strings, and the server-side app will have to parse them into correct data types. Line 19 sends the `HTTP` `POST` request when the request content is ready. Line 22 receives the `HTTP` response that contains the URL of the uploaded image if the upload has success. ![Demo Application calls the API to upload an image - Upload/Download Files Using HttpClient](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/httpclient.gif?resize=640%2C350&ssl=1)Demo Application calls the API to upload an image ## Check the uploaded type It is important to check what kind of file the procedure is going to save on the file system to avoid attach of any kind. For this reason, in the `FileHelpers` I want to check the header of the uploading file to be sure is a genuine file. So, for that, we can read the header of a file. If you see the code of this helper, you see this definition: ``` private static readonly Dictionary FileSignature = new Dictionary { { ".gif", new List { new byte[] { 0x47, 0x49, 0x46, 0x38 } } }, { ".png", new List { new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A } } } } ``` So, the procedure receives from the `FormData` the image in `byte[]`. Then, it is easy to check the first characters for a specific extension. If the file starts with the expected signature, the file is valid. How to know the signature of an extension? There is an amazing website for that: [File Signature Database](https://www.filesignatures.net/index.php?page=search). If you search for `gif` extension, the website gives the right signature ![Signature for GIF on File Signature website - Upload/Download Files Using HttpClient](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/image.png?resize=640%2C353&ssl=1)Signature for GIF on File Signature website Then, I added in the `FileSignature` a new `Dictionary` with the list of bytes for a specific file type, such as GIF that has a signature 47 49 46 38. ## Wrap up In conclusion, this is a good example how to upload/download Files using HttpClient and the full source code is on [GitHub](https://github.com/erossini/HttpClientMultipart). Also, very soon, I will release a new component for Blazor that implement a [Markdown Editor](https://puresourcecode.com/dotnet/blazor/markdown-editor-with-blazor/) to replace the simple and current one. Stay tuned! **Categories:** .NET Core, .NET6 **Tags:** httpclient, mime, upload, webapi --- ### [Handling exceptions globally with NET6](https://puresourcecode.com/dotnet/net6/handling-exceptions-globally-with-net6/) **Published:** January 1, 2022 **Author:** Enrico **Excerpt:** In this new post, I like to show how handling exceptions globally with .NET6 adding a single class using a generic exception middleware **Content:** In this new post, I like to show how [handling exceptions](https://puresourcecode.com/net-core/exception-handling-in-asp-net-mvc/) globally with NET6 adding a single class. Generally, we handled all expected exceptions with try-catch wherever necessary in the code. When our code throws an exception, we need to map that exception. An exception is thrown in the lower layer like data access. You need to map that for all the layers until the presentation layer. Or you have a bigger problem, losing the exception. Usually, you return a [**HTTP 500** status code](https://www.tutorialspoint.com/http/http_status_codes.htm), an internal error. But no one knows the error until checking the logging/tracing. Assuming you have logging/tracing. So, having a global handling exception allows you to not be concerned about mapping every exception or mapping between layers. ``` public static class ExceptionMiddleware { public static void ConfigureExceptionHandler(this IApplicationBuilder app, bool isDev) { app.UseExceptionHandler(appError => { appError.Run(async context => { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.ContentType = "application/json"; var contextFeature = context.Features.Get(); if (contextFeature != null) { var ex = contextFeature?.Error; await context.Response.WriteAsync(JsonConvert.SerializeObject( new ProblemDetails { Type = ex.GetType().Name, Status = (int)HttpStatusCode.InternalServerError, Instance = contextFeature?.Path, Title = isDev ? $"{ex.Message}" : "An error occurred.", Detail = isDev ? ex.StackTrace : null })); } }); }); } } ``` The function `ConfigureExceptionHandler` receives the `IApplicationBuilder` and a variable `isDev` from the caller. In line 9, we return the HTTP status code 500, so the front end can be aware of the error. If the environment is Development, the error message will have the **StackTrace** (line 24) and the exception message (line 23). But here, you can customize your error message. You also can use another object. You are not stuck with **ProblemDetails** if you want a different object with different fields and logic, you can do it. **ProblemDetails** is defined in `Microsoft.AspNetCore.Http.Extensions` ## Register the ConfigureExceptionHandler Now, we need to register the middleware goes to the `Program.cs` or `Startup.cs` and type this. ``` public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider) { // ... Middlewares(app, env); } void Middlewares(IApplicationBuilder app, IWebHostEnvironment env) { app.ConfigureExceptionHandler(env.IsDevelopment()); } ``` After that, if you run your API in development, in case of an internal server error, you get a `Json` like the following image. ![Error in the web api in the development environment - Handling exceptions globally with .NET6](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/14VkqRPeQbolq_eYfwq7EEw.png?w=640&ssl=1)Error in the web api in the development environment So, the same code in production returns the following `Json`. ![Error in the web api in the production environment - Handling exceptions globally with .NET6](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2022/01/1mf2T-aJQ-LAvxuD51cvYNg.png?w=640&ssl=1) Error in the web api in the production environment ## Wrap up In conclusion this is how handling exceptions globally with NET6. Please leave your comment below. Happy new year! **Categories:** .NET6 **Tags:** aspnet-core, net6, webapi --- ### [Start with Unity 2021](https://puresourcecode.com/tools/unity/start-with-unity-2021/) **Published:** December 6, 2021 **Author:** Enrico **Excerpt:** In this new post I explain how to start with Unity 2021 and the basic interaction with the Unity Editor, navigate the functions of the editor **Content:** In this new post I explain how to start with Unity 2021 and the basic interaction with the Unity Editor, navigate the functions of the editor. ## Why Unity? C# is one of the most popular programming languages which is used to create games in the Unity game engine. Experiences (games, AR/VR apps, etc) built with Unity have reached nearly [3 billion devices worldwide](https://unity3d.com/public-relations). Why is C# widely-used to create games? How does it compare to C++? How is C# being used in other areas such as mobile and web development? I think Unity chose to move forward with [C#](https://puresourcecode.com/category/dotnet/csharp/) instead of [Javascript](https://puresourcecode.com/tag/javascript/) or Boo because of its learning curve and its history with [Microsoft](https://puresourcecode.com/category/news/microsoft/). \[[Boo](https://blogs.unity3d.com/2014/09/03/documentation-unity-scripting-languages-and-you/) was one of the three scripting languages for the Unity game engine until it was dropped in 2014\]. In my experience, C# is easier to learn than languages like C++, and that accessibility is a huge draw for game designers and programmers in general. With Xamarin mobile development and ASP.NET web applications in the mix, there’s really no stopping the C# language any time soon. ## Install Unity (Windows) First, go to the [Unity website](https://unity.com/) and select the button **Get started** (see screenshot below). ![Unity home page - Start with Unity 2021](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image.png?resize=640%2C429&ssl=1)Unity home page This will take you to the [Unity store page](https://store.unity.com/). This page looks like the screenshot below. Select the **Individual** option. The other paid options offer more advanced functionalities and services, but you can check these out on your own. ![Unity Store - Start with Unity 2021](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-1.png?resize=640%2C429&ssl=1)Unity Store I chose Personal and then click on **Get started** again. Download the app and install it. Then, the installer asks you to download the **Unity Editor**. Click on **Install Unity Editor**. ![Install Unity Editor - Start with Unity 2021](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-2.png?resize=640%2C473&ssl=1)Install Unity Editor Then, you have to accept the licence before proceeding. ![Accept personal licence - Start with Unity 2021](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-3.png?resize=640%2C473&ssl=1)Accept personal licence Downloading and installing all the components require between 15 and 30 minutes. So, take a cup of tea and see you in a bit. ## Install Unity (Mac) So, you can install Unity also for Mac. From the [Unity website](https://unity.com/download), under **Download**, you can download the Unity Hub V3 for Mac. ![Install Unity for Mac](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/Install-Unity-for-Mac.png?resize=640%2C388&ssl=1)Install Unity for Mac Then, launch the **Unity Hub** installer and the first think you have to do is to accept the licence. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/Unity-licence.png?resize=640%2C484&ssl=1) Then, copy **Unity Hub** in the application folder. ![Copy Unity Hub in the Applications folder](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/Copy-Unity-applicaton-folder.png?resize=640%2C483&ssl=1)Copy Unity Hub in the Applications folder Now, you can go to the Applications folder and run **Unity** but before you have to allow macOS to open the application because it was downloaded from the internet and not from the Apple Store. ![Allow to open the Unity Hub on Mac](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/Screenshot-2021-12-06-at-19.08.28.png?resize=640%2C695&ssl=1)Allow to open the Unity Hub on Mac Finally, you have your **Unity Hub** open. ![First time Unity Hub is opinion Mac](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/Unity-Hub-Open-First-time.jpg?resize=640%2C401&ssl=1)First time Unity Hub is opinion Mac Now, you have to create a new account or use an existing one. After that, like in the Windows version, you have to accept the licence, again, and download the **Unity Editor**. ## Creating a new project When you launch the Unity Hub application, if you have created an account, you have a button on the top right “**New project**“. ![Unity Hub home page](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-4.png?resize=640%2C473&ssl=1)Unity Hub home page If you click on the button “New project”, you have few choices. As you see, you can download different templates like the Lego templates. ![Creating a new project with Unity](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-5.png?resize=640%2C473&ssl=1)Creating a new project with Unity So, for this first project I select **3D** as template, `First Project` as **Project Name** and then the **Location**. ![Creating a new project's fields](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-6.png?resize=640%2C473&ssl=1)Creating a new project’s fields With the project created, you’re all set to explore the Unity interface! You can re-open your project anytime from the **Projects** panel in **Unity Hub**. ## Navigation the editor When the new project finishes initializing, you’ll see the glorious Unity Editor! I’ve marked the important tabs (or panels, if you prefer) in the following screenshot: ![First project with Unity - Explain](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-8.png?resize=640%2C408&ssl=1)First project with Unity – Explain So, let me explain what we have in front of us. I added the number so it is easy to explain. ![Unity windows explain](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/B17573_01_14.png?w=640&ssl=1)Unity windows explain 1. The **Toolbar** panel is the topmost part of the Unity Editor. From here, you can manipulate objects (far-left button group) and play and pause the game (center buttons). The rightmost button group contains Unity services, **Layer Masks**, and layout scheme features, which we won’t be using in this book because they don’t apply to learning C#. 2. The **Hierarchy** window shows every item currently in the game **scene**. In the starter project, this is just the default camera and directional light, but when we create our prototype environment, this window will start to get filled in. 3. The **Game** and **Scene** windows are the most visual aspects of the editor. Think of the **Scene** window as your stage, where you can move and arrange 2D and 3D objects. When you hit the **Play** button, the **Game** window will take over, rendering the **Scene** view and any programmed interactions. 4. The **Inspector** window is your one-stop shop for viewing and editing the properties of objects in the scene. If you select the **Main Camera** **GameObject** in the **Hierarchy**, you’ll see several parts (Unity calls them components) are displayed—all of which are accessible from here. 5. The **Project** window holds every asset that’s currently in your project. Think of this as a representation of your project’s folders and files. 6. The **Console** window is where any output we want our scripts to print will show up. From here on out, if we talk about the console or debug output, this panel is where it will be displayed. ### Re-open default windows If any of these windows get closed by accident, you can re-open them anytime from **Unity** | **Window** | **General**. ![Re-open Unity General windows](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-9.png?resize=640%2C507&ssl=1)Re-open Unity General windows ### Set up Visual Studio (Windows) Before continuing, it’s important that Visual Studio is set up as the script editor for your project. Go to the **Unity menu** | **Preferences** | **External Tools** and check that **External Script Editor** is set to Visual Studio for Mac or Windows: ![Unity Preferences menu and the main External Tools window](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-10.png?resize=640%2C506&ssl=1)Unity Preferences menu and the main External Tools window So, in the following screenshot you see my settings for Windows and Visual Studio 2011. ![External tools for Unity in Windows](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-11.png?resize=640%2C546&ssl=1)External tools for Unity in Windows ### Set up Visual Studio for Mac If you are using a Mac, you find the **External Tools** under the **Unity** menu and then **Preferences…** ![Unity Preferences for Mac](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/Unity-Mac-Preferences-1.png?resize=640%2C333&ssl=1)Unity Preferences for Mac ### Change theme Then, if you want to change the **Editor Theme**, under **General**, you can select **Dark** or **Light**. ![Change Unity Editor theme](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-12.png?resize=640%2C547&ssl=1)Change Unity Editor theme ## Using C# with Unity Now, going forward, it’s important to think of Unity and C# as symbiotic entities. Unity is the engine where you’ll create scripts and game objects, but the actual programming takes place in [Visual Studio](https://puresourcecode.com/tag/visualstudio-2022/). ### Working with C# scripts So, even though we haven’t covered any basic programming concepts yet, they won’t have a home until we know how to create an actual C# script in Unity. A C# script is a special kind of C# file in which you’ll write C# code. These scripts can be used in Unity to do virtually anything, from responding to player input to creating game mechanics. There are several ways to create C# scripts from the editor: - Select **Assets** | **Create** | **C# Script** - Right under the **Project** tab, select the **+** icon and choose **C# Script** - Right-click on the **Assets** folder in the **Project** tab and select **Create** | **C# Script** from the pop-up menu - Select any `GameObject` in the **Hierarchy** window and click **Add Component** | **New Script** ![Unity project creates C# script](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-13.png?resize=640%2C988&ssl=1)Unity project creates C# script For the sake of organization, we’re going to store our various assets and scripts inside their marked folders. This isn’t just a Unity-related task—it’s something you should always do, and your coworkers will thank you (I promise): 1. From the **Project** tab, select **+** | **Folder** (in the following screenshot I’ve selected **Assets** | **Create** | **Folder**) and name it `Scripts` ![Create a folder for C# scripts](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-14.png?resize=640%2C666&ssl=1)Create a folder for C# scripts 2. Double-click on the **Scripts** folder and create a new C# script. By default, the script will be named `NewBehaviourScript`, but you’ll see the filename highlighted, so you have the option to immediately rename it. Double-click on the file will open Visual Studio immediately. The boilerplate code you see is ``` using System.Collections; using System.Collections.Generic; using UnityEngine; public class NewBehaviourScript : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } ``` ## Exploring the documentation The last topic we’ll touch on in this first foray into Unity and C# scripts is documentation. It’s important to form good habits early when dealing with new programming languages or development environments. ### Accessing Unity’s documentation So, once you start writing scripts in earnest, you’ll be using Unity’s documentation quite often, so it’s beneficial to know how to access it early on. The *Reference Manual* will give you an overview of a component or topic, while specific programming examples can be found in the *Scripting Reference*. Every game object (an item in the **Hierarchy** window) in a scene has a **Transform** component that controls its **Position**, **Rotation**, and **Scale**. To keep things simple, we’ll just look up the camera’s **Transform** component in the Reference Manual: 1. In the **Hierarchy** tab, select the **Main Camera** game object 2. Move over to the **Inspector** tab and click on the information icon (question mark) at the top right of the **Transform** component: ![Help in Unity](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/12/image-15.png?resize=640%2C696&ssl=1)Help in Unity **Categories:** Unity **Tags:** csharp, games, unity **Hashtags:** c#, mac, unity, windows, windows10, windows11 --- ### [Bill Gates predicts the future in 1994](https://puresourcecode.com/news/bill-gates-predicts-the-future-in-1994/) **Published:** November 26, 2021 **Author:** Enrico **Excerpt:** Microsoft released Windows 3! Picture of 19yo Bill Gates and 22yo Paul Allen from February of 1975, show to the world BASIC, the language **Content:** This video’s washed-out colors suggest its own fading over time, even before the ’90s-era sound of synthesizer music and electronic drums kick in. A mellifluous announcer croons, “Microsoft — a name and reputation known around the world,” as the video shows footage of a flag flying outside the company’s Redmond headquarters with the logo for Microsoft Windows. And then it touts the company’s still-strange-in-1994 vision of “a computer on every desk, in every home…” —Before adding the often-forgotten ending of that sentence: “…running Microsoft software.”) It’s all a forgotten glimpses of a world from long ago, and a chance to look back into the earliest days of personal computing. But maybe it also offers a chance to get a fresh perspective on our own modern world of computing — by remembering at least some of how it all got started ## Waiting for Windows The video boasts of a young company that employs “thousands” of people worldwide. (Today Microsoft employs [an estimated 181,000 people](https://www.statista.com/statistics/273475/number-of-employees-at-the-microsoft-corporation-since-2005/), according to market data site Statista.) And soon, there it is — the moment you’ve been waiting for — Windows circa 1994. It’s version 3. ![Windows 3 (circa 1994) - screenshot from 1994 Welcome to Microsoft video (via Computer History Archive Project's YouTube channel)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/1d996b28-windows-3-circa-1994-screenshot-from-1994-welcome-to-microsoft-video-via-computer-history-archive-projects-youtube-channel-300x193.png?resize=300%2C193&ssl=1) A Windows user smiles knowingly, then clicks on a desktop icon labeled the 70’s, bringing up a picture of 19-year-old Bill Gates and 22-year-old Paul Allen from February of 1975. It’s shortly after they’d created the BASIC programming language (along with Monte Davidoff) for the Altair 8800, which the announcer touts as the first programming language created for a personal computer. There’s shots of a ’70s-era Altair 8800 computer, and even its documentation (with its blocky all-caps cover page). “Bill and Paul knew they were onto something important: a personal computer,” the announcer says. And then suddenly, there’s footage of Bill Gates himself. It’s now 1994 — Bill is 39 — and his memories of the 1970s are crystal clear. “Even at that point, we had thought ‘Wow, this microprocessor is going to do something incredible,’” Gates remembers. “As early as 1971, Paul and I had talked about the microprocessor, and it was really his insight, that because of semiconductor improvements things would just keep getting better. “And so, I said to him, ‘Whoa, exponential phenomena is pretty rare and dramatic. Are you serious?’” “They were serious,” the announcer jumps in, “and quickly realized these strange little boxes were nothing without powerful and useful programs!” Then, putting it even more succinctly, he sums up the tsunami that was about to come with three words. “Hardware needs software.” ![39-year-old Bill Gates - Screenshot from 1994 Welcome to Microsoft video (via Computer History Archive Project's YouTube channel)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/b3eed044-39-year-old-bill-gates-screenshot-from-1994-welcome-to-microsoft-video-via-computer-history-archive-projects-youtube-channel-300x169.png?resize=300%2C169&ssl=1) Other milestones flash by — the formation of Microsoft Far East to handle international sales, the creation of Microsoft’s own implementation of the Fortran and Cobol programming languages. “Within a year, the Microsoft BASIC compiler was running on virtually every computer,” the announcer adds nonchalantly. And after one last milestone — the opening of new offices in Bellevue, Washington — the smiling Windows users click over to a folder named “The 80s.” ## 16-Bit Computers and MS-DOS The announcer then casts his gaze back 13 years to a moment that now seems like ancient history. “In 1981, it was time to take a new step. Microsoft reorganized as a privately-held corporation with Bill Gates as president and Paul Allen as vice president.” “We knew that 16-bit computing was on its way,” Gates says, “and we saw that it could be a good business machine, and we decided to focus a lot of early work on that Intel chip.” The video calls out August 2nd, 1981 as another milestone — the day [IBM](https://www.ibm.com/cloud?utm_content=inline-mention) released a personal computer running Microsoft’s “16-bit operating system, MS-DOS, version 1.0.” (Also included was the BASIC programming language.) Other milestones include the launch of subsidiaries in France, Germany, and the UK, and the creation of the book-publishing arm Microsoft Press. ![MS-DOS - Screenshot from 1994 Welcome to Microsoft video (via Computer History Archive Project's YouTube channel)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/dd1e9726-ms-dos-screenshot-from-1994-welcome-to-microsoft-video-via-computer-history-archive-projects-youtube-channel-300x169.png?resize=300%2C169&ssl=1) And while we’re remembering days gone by, the video cuts to a short but thought-provoking archival sound clip of Douglas Engelbart, the original creator of the mouse, in which Engelbart admits “I don’t know why we call it a mouse. “Sometimes I apologize. It started that way, and we never did change it.” “We continued to evolve the design,” Bill Gates says, “going to a sleeker and sleeker appearance design,” as the video cuts through several early iterations of the Microsoft mouse. (And not a one of them is wireless…) There are more glimpses of forgotten technologies when the announcer remembers the “innovative marketing strategy” for the introduction of Microsoft Word for MS-DOS in 1983: “A demo disk was included in an issue of PC World magazine.” Floppy disks later gave way to CD-ROMs, and eventually just to online software-as-a-service. But instead, this video’s announcer takes us backward in time, to a world where Microsoft Windows “was announced at the 1983 Comdex tradeshow. But it would be two long years before it would be released.” The ruthless competition between Apple computers seems glossed over, with Gates saying casually and even admiringly that Apple “bet their company” on graphical interfaces, adding almost after-the-fact that “that’s why we got so involved in building applications for the Macintosh early on — we thought they were right. And we really bet our success on it as well.” Other moments in history flit by just as breezily, like a scrapbook that’s been pulled off a dusty shelf. Microsoft Flight Simulator — first introduced in 1982 — become the world’s best-selling game for personal computers. There’s Microsoft’s IPO in 1986 (when “a new force in American business was launched.”) 30-year-old Bill Gates makes in onto the cover of Fortune magazine. There’s even time-lapse photography that actually shows the construction of their new corporate headquarters in Redmond in 1985. ![Microsoft Redmond campus - screenshot from 1994 Welcome to Microsoft video (via Computer History Archive Project's YouTube channel)](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/fd7538eb-microsoft-redmond-campus-screenshot-from-1994-welcome-to-microsoft-video-via-computer-history-archive-projects-youtube-channel-300x227.png?resize=300%2C227&ssl=1) And soon the video’s Windows user clicks on the ’90s folder. “In April of 1990, a Russian version of MS-DOS was released, making a total of 14 language versions available,” the announcer tells us. In footage showing the announcement of Windows version 3, Gate dubs it “a major milestone in the history of the PC industry!” Bill Neukom, Microsoft’s then vice president of legal and corporate affairs appears, and points out it was the culmination of a full eight years of Windows development. Fall of 1990 finds Gates making a “visionary address” to Comdex. And what made it visionary? “Bill Gates put forward the idea of an information highway,” the announcer explains. ## The Road Ahead The 1994 video argues Gates was ahead of the curve, intently telling the audience at Comdex that year that “We need a vast array of information to be available to users” and urging hardware and software makers — as well as distribution channels — to come together to realize “the vision of information at your fingertips.” The peppy music continues, as the announcer tells us Windows 3.0 became “a worldwide standard in computer interfaces” — although it was only available in 12 languages, and in 24 countries. Predictably the next milestone is the release of Windows 3.1 in May of 1992. Six weeks after its release, Microsoft had sold… 3 million copies. (Which the announcer reminds us was “a number unprecedented in the industry.”) Now we reach 1993, a year in which “Home computing is rapidly growing, and half of all homes with personal computers have school-age children.” But we’ve reached a moment that sums up a much of the future to come with just one sentence. “As those children grow, a new generation of computer users will create a new demand for new tools and new products.” it’s a year when 40 million PCs are running Microsoft Windows. The announcer tells us it’s “the standard of choice among personal computer users worldwide.” Microsoft programs are now published in a whopping 28 languages. And finally the Windows user in the 1994 video clicks on a folder labeled “The Future.” It pulls up a pre-release version of the now-forgotten [*Microsoft Space Simulator*](https://en.wikipedia.org/wiki/Microsoft_Space_Simulator). “As we look ahead, Microsoft will remain on the cutting edge of software development,” the announcer says. Over a driving backbeat, the music changes to a more futuristic-sounding synthesizer, as the announcer promises a “shifting emphasis” to home services (while continuing to serve the corporate marketplace). But the video is still showing clunky desktop computers with floppy disk drives. ## The Future ![Screenshot from 1994 Welcome to Microsoft video (via Computer History Archive Project's YouTube channel](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/11/3e44e46a-screenshot-from-1994-welcome-to-microsoft-video-via-computer-history-archive-projects-youtube-channel-300x225.png?resize=300%2C225&ssl=1) In a weird way, Gates misses part of the future, imagining a burgeoning market for software publishing but without foreseeing the ultimate prominence of online applications — and, eventually, data storage and at-scale data analytics. And “It was that ‘computer on every desk’ that led them to miss the smartphone,” quips one YouTube commenter. Gates seems to be thinking about a world where we gather together in our cozy dens, enjoying space simulators and family-finance spreadsheets. There he is, wearing an earth-toned sweater and sharing a wholesome vision of making products that are enjoyable and useful and “draw in the whole family, so that these devices in the home are really something that are very, very worthwhile.” There’s a comforting optimism in this moment, as the announcer concludes this segment on the future by saying cheerily that “The Microsoft vision of a computer on every desk and in every home — in this country and around the world — is becoming a reality.” Of course, what the video didn’t mention would be the release of Windows 95 in the following year, which in fact brought Microsoft very close to this goal, a goal that seemed radical then and quaint now. With breezy corporate enthusiasm, the announcer anticipates, without a hint of concern, a world where “a vast universe of information will be at the fingertips of everyone.” “I think there’s an absolutely incredible opportunity here,” Gates says. “And I think it’s going to be very exciting.” **Categories:** Microsoft, News **Tags:** basic, bill-gates, windows --- ### [Install MAUI with Visual Studio 2022](https://puresourcecode.com/dotnet/net-core/install-maui-with-visual-studio-2022/) **Published:** July 16, 2021 **Author:** Enrico **Excerpt:** Microsoft is talking a lot about this new technology and here I will explain how to install MAUI with Visual Studio 2022 and run a first app **Content:** Microsoft is talking a lot about this new technology and here I will explain how to install MAUI with [Visual Studio](https://puresourcecode.com/category/tools/visual-studio-tools/) 2022 and run a first app. > Update: I create a new post [Install MAUI with Visual Studio 2022 (Preview)](https://puresourcecode.com/dotnet/net-core/install-maui-with-visual-studio-2022-preview/) after Microsoft released Visual Studio 2022 and a new preview of MAUI. First of all, in my point of view, the most important question is: is Xamarin dead? Just few days ago, [James Montemagno](https://montemagno.com/) released the final Xamarin Podcast and they said: > So we are now what do we call a network load? This is it’s all backed by Nougat in that Nougat is the infrastructure by which we deliver all of our installs for Android, iOS, Mac OS as well as Maui Blazer and all that sort of thing. > > So with the net installer, you can actually install optional workloads, of which Maui is now one. So if you are a command line junkie and you really, really enjoy getting getting your fingers dirty with the keystrokes, then this is for you so you can do a network load. > > Install command and pass Maui as the workload ID and it will go out and grab all the SDK’s that you need to be able to run a Maui application. > > The only thing it’s not going to do for you is get your Android emulators, your iOS, Xcode installed, and some of those other third party dependencies, but in terms of all the net things it’s going to do that for you. Now. The good news is jumping to the other side, the very familiar experience. This is all still. Part of the Visual Studio installer experience. So when you go install Visual Studio in a future release, this isn’t there. > > The Final Xamarin Podcast, James Montemagno and David Ortinau ![](https://i0.wp.com/media24.fireside.fm/file/fireside-images-2024/podcasts/images/3/306e7564-d5eb-4af3-b3b2-e6aa1f21a9ce/cover.jpg?w=640&ssl=1) ## Episode 126: .NET 9, Holiday Hacks, & GitHub Copilot Free[ ](https://www.dotnetmauipodcast.com/126) – [ The .NET MAUI Podcast ](https://www.dotnetmauipodcast.com/) .NET 9 is officially here! We talk a little bit .NET Conf and the major announcements, .NET Aspire, AI, Holiday Hacks, and GitHub Copilot Free! Links: .NET Conf: https://www.youtube.com/playlist?list=PLdo4fOcmZ0oXeSG8BgCVru3zQtw_K4ANY .NET Aspire: https://learn.microsoft.com/dotnet/aspire/get-started/aspire-overview Microsoft Extensions AI: https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/ GitHub Copilot Free: https://github.blog/news-insights/product-news/github-copilot-in-vscode-free/ Visual Studio – Copilot Blog: https://code.visualstudio.com/blogs/2024/12/18/free-github-copilot Visual Studio Code – Copilot Blog: https://devblogs.microsoft.com/visualstudio/github-copilot-free-is-here-in-visual-studio/ Follow Us: * James: Twitter (https://twitter.com/jamesmontemagno), Blog (https://montemagno.com), GitHub (http://github.com/jamesmontemagno), Merge Conflict Podcast (http://mergeconflict.fm) * Matt: Twitter (https://twitter.com/codemillmatt), Blog (https://codemilltech.com), GitHub (https://github.com/codemillmatt) * David: Twitter (https://twitter.com/davidortinau), Github (https://github.com/davidortinau) ## Table of contents - [What is .NET MAUI?](#h-what-is-net-maui) - [Who .NET MAUI is for](#who-net-maui-is-for) - [How MAUI works](#how-net-maui-works) - [Architecture](#h-architecture) - [MAUI installation preview](#h-maui-installation-preview) - [Install .NET 6 Preview](#install-net-6-preview-6) - [Basic project with MAUI](#h-basic-project-with-maui) - [Simplify development](#simplify-development) - [Startup](#h-startup) - [Register fonts](#register-fonts) - [Application on Android](#h-application-on-android) - [Application on Windows](#h-application-on-windows) ## What is .NET MAUI? As far as I know, Microsoft started to talk about MAUI from last year in May and I [created a post for that](https://puresourcecode.com/news/introducing-net-multi-platform-app-ui/). Now, with Visual Studio 2022, it seems MAUI is rolling out and the preview is ready to download and install. .NET Multi-platform App UI (MAUI) is a cross-platform framework for creating native mobile and desktop apps with C# and XAML. Using MAUI, you can develop apps that can run on Android, iOS, macOS, and Windows from a single shared code-base. ![.NET Multi-platform App UI (MAUI) - Install MAUI with Visual Studio 2022](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/maui.png?resize=435%2C387&ssl=1).NET Multi-platform App UI (MAUI) .NET MAUI is open-source and is the evolution of Xamarin.Forms, extended from mobile to desktop scenarios, with UI controls rebuilt from the ground up for performance and extensibility. If you’ve previously used Xamarin.Forms to build cross-platform user interfaces, you’ll notice many similarities with .NET MAUI. However, there are also some differences. Using .NET MAUI, you can create multi-platform apps using a single project, but you can add platform-specific source code and resources if necessary. One of the key aims of .NET MAUI is to enable you to implement as much of your app logic and UI layout as possible in a single code-base. ## Who .NET MAUI is for .NET MAUI is for developers who want to: - Write cross-platform apps in XAML and C#, from a single shared code-base in Visual Studio. - Share UI layout and design across platforms. - Share code, test, and business logic across platforms. ## How MAUI works First, MAUI unifies Android, iOS, macOS, and Windows APIs into a single API that allows a write-once run-anywhere developer experience, while additionally providing deep access to every aspect of each native platform. So, .NET6 provides a series of platform-specific frameworks for creating apps for Android, iOS, macOS, and Windows UI (WinUI) Library. These frameworks all have access to the same .NET 6 Base Class Library (BCL). This library abstracts the details of the underlying platform away from your code. The BCL depends on the .NET runtime to provide the execution environment for your code. For Android, iOS, and macOS, the environment is implemented by Mono, an implementation of the .NET runtime. On Windows, WinRT performs the same role, except it’s optimized for the Windows platform. While the BCL enables apps running on different platforms to share common business logic, the various platforms have different ways of defining the user interface for an app, and they provide varying models for specifying how the elements of a user interface communicate and interoperate. You can craft the UI for each platform separately using the appropriate platform-specific framework (.NET for Android, one for iOS, one for macOS, or WinUI), but this approach then requires you to maintain a code-base for each individual family of devices. ### Architecture Before to install MAUI with Visual Studio 2022, we have to understand the MAUI architecture. So, .NET MAUI provides a single framework for building the UIs for mobile and desktop apps. The following diagram shows a high-level view of the architecture of a .NET MAUI app: ![MAUI architecture - Install MAUI with Visual Studio 2022](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/maui-architecture.png?resize=640%2C405&ssl=1)MAUI architecture Then, .NET MAUI apps can be written on PC or Mac, and compile into native app packages: - Android apps built using .NET MAUI compile from C# into intermediate language (IL) which is then just-in-time (JIT) compiled to a native assembly when the app launches. - iOS apps built using .NET MAUI are fully ahead-of-time (AOT) compiled from C# into native ARM assembly code. - macOS apps built using .NET MAUI use Mac Catalyst, a solution from Apple that brings your iOS app built with UIKit to the desktop, and augments it with additional AppKit and platform APIs as required. - Windows apps built using .NET MAUI use Windows UI Library (WinUI) 3 to create native apps that can target the Windows desktop and the Universal Windows Platform (UWP). For more information about WinUI, see [Windows UI Library](https://docs.microsoft.com/en-us/windows/apps/winui/). ## MAUI installation preview So, how to Install MAUI with Visual Studio 2022 preview? Remember that this post is related to Visual Studio 2022 Preview and MAUI preview. You can download Visual Studio 2022 for [here](https://visualstudio.microsoft.com/vs/preview/vs2022/). To create .NET MAUI apps in Visual Studio, you’ll also need [Visual Studio 2022 Preview 2](https://visualstudio.microsoft.com/vs/preview/vs2022/) with the following workloads installed: - Mobile development with .NET - Universal Windows Platform development - Desktop development with C++ - .NET Desktop Development - ASP.NET and web development (required for Blazor Desktop and the `BlazorWebView` control) ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/vs-workloads.png?w=640&ssl=1)Visual Studio 2022 Setup – What do you have to check for MAUIIn addition, you must currently install the following Visual Studio extension to create apps that target Windows UI Library (WinUI) 3: - [Single-project MSIX Packaging Tools](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) Then, for more information about the required workloads and components for WinUI 3 development, see [Required workloads and components](https://docs.microsoft.com/en-us/windows/apps/project-reunion/set-up-your-development-environment#required-workloads-and-components). Now, to use the `WebView` or `BlazorWebView` controls on Windows you need to install the WebView2 package: - [Microsoft Edge WebView2 installer](https://developer.microsoft.com/microsoft-edge/webview2/) ### Install .NET 6 Preview First, to verify your development environment, and install any missing components, use the [maui-check](https://github.com/Redth/dotnet-maui-check) utility. For acquiring and installing .NET SDKs, `maui-check` uses the same workload commands described in the [release notes](https://github.com/dotnet/core/blob/main/release-notes/6.0/install-maui.md). Install the `maui-check` utility using the following .NET CLI command: ``` dotnet tool install -g redth.net.maui.check ``` So, you have to see this screen. ![Install .NET MAUI from Windows PowerShell - Install MAUI with Visual Studio 2022](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/image.png?resize=640%2C351&ssl=1)Install .NET MAUI from Windows PowerShell Then, run `maui-check`: ``` maui-check ``` Now, this command will open a new [Windows Terminal](https://puresourcecode.com/news/getting-started-with-windows-terminal/) and install all the components for MAUI. If the process finds an issue to install it, it will ask you if you want to try to fix automatically. Then, type `Y` and the process will install all the requirements. You have my screenshot here. ![.NET MAUI installation - Install MAUI with Visual Studio 2022](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/image-1.png?resize=640%2C647&ssl=1).NET MAUI Installation process ## Basic project with MAUI First, when you open Visual Studio 2022 Preview, there is no option to create a MAUI project yet. So, we have to use the prompt. Open a command prompt and create a new project by running the command: ``` dotnet new maui -n HelloMaui ``` This command creates a new folder with the test application. ![Folder with the MAUI project](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/image-4.png?resize=640%2C361&ssl=1)Folder with the MAUI project Now, open Visual Studio 2022 Preview and select **Open a project or solution** and select the test project you have just created. ![](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/image-5.png?resize=640%2C423&ssl=1)Open a project or solution in Visual Studio 2022 The solution takes a while to open. So, the first thing I noticed is the project structure. There are 2 projects: one for Windows (WinUI) and another one for Android, iOS and macOS. So, in `HelloMaui` project, there is a **Platforms** folder that contains the specific code for each platform. In the **Resources** folder, there are **Fonts** and **Images** folders. In the HelloMaiu.WinUI, you can see there are a lot of links to the `HelloMaui` project (for example `Resources`, `App.xaml`, `MainPage.xml`, `Startup.cs`). ![MAUI Solution Explorer](https://i0.wp.com/puresourcecode.com/wp-content/uploads/2021/07/image-8.png?resize=404%2C1024&ssl=1)MAUI Solution Explorer ### Simplify development Single project is built on top of a collection of experiences that are being simplified in .NET 6. The following list shows the experiences that will be shared in .NET MAUI single project: - Resources - Images - Fonts - App icons - Splash screens - Raw Assets - App manifest - NuGet - Platform-specific code All other features are being moved from their own platform-projects into platform folders in the single project. ### Startup So, MAUI apps are bootstrapped using the [.NET Generic Host](https://docs.microsoft.com/en-us/dotnet/core/extensions/generic-host). This enables apps to be initialized from a single location, and provides the ability to configure fonts, services, and third-party libraries. Each platform has an entry point that initializes the app host builder, and then invokes the `Configure` method of the `Startup` class in your app. The `Startup` class can be considered the entry point for your app, and is responsible for creating a window that defines the initial page of your app. The `Startup` class, which must implement the `IStartup` interface, must at a minimum provide an app to run: ``` using Microsoft.Maui; using Microsoft.Maui.Hosting; public class Startup : IStartup { public void Configure(IAppHostBuilder appBuilder) { appBuilder .UseMauiApp(); } } ``` The `App` class should derive from the `Application` class, and must override the `CreateWindow` method to provide a `Window` within which your app runs, and that defines the UI for the initial page of the app: ``` using Microsoft.Maui; using Microsoft.Maui.Controls; public partial class App : Application { protected override IWindow CreateWindow(IActivationState activationState) { return new Window(new MainPage()); } } ``` In the example above, `MainPage` is a `ContentPage` that defines the UI for the initial page of the app. ### Register fonts Fonts can be added to your app and referenced by filename or alias. This is accomplished by invoking the `ConfigureFonts` method on the `IAppHostBuilder` object. Then, on the `IFontCollection` object, call the `AddFont` method to add the required font: ``` using Microsoft.Maui; using Microsoft.Maui.Hosting; public class Startup : IStartup { public void Configure(IAppHostBuilder appBuilder) { appBuilder .UseMauiApp() .ConfigureFonts(fonts => { fonts.AddFont("Lobster-Regular.ttf", "Lobster"); }); } } ``` In the example above, the first argument to the `AddFont` method is the font filename, while the second argument represents an optional alias by which the font can be referenced when consuming it. Any custom fonts consumed by an app must be included in your `.csproj` file. This can be accomplished by referencing their filenames, or by using a wildcard: ``` ``` > If you add fonts with Visual Studio, it adds them automatically. You can use the font by referencing its name, without the file extension: ``` ``` Alternatively, you can reference to it with an alias: ```