The Rise of Ancient Kingdoms
Once upon a time in the software world, two great kingdoms reigned supreme: Visual Basic and C++. The most powerful representative of these kingdoms was a knight named Microsoft Foundation Classes (MFC). MFC made the complexity of the Windows API more understandable for developers, making window management and user interface elements more accessible. However, behind these kingdoms, there was a hidden force that formed the foundation of the software world: COM (Component Object Model). This mysterious force simplified complexities, enabled software components to interact with each other, and ensured that different programming languages could work together harmoniously in the same environment.
Microsoft Foundation Classes (MFC) is a powerful framework developed by Microsoft that provides a set of libraries and tools for software developers to create graphical user interface (GUI) applications for the Windows operating system.
This visual was made by me using Visme.
Born in 1993, COM paved the way for technologies like OLE, ActiveX, and COM+. However, the power of COM relied on an ancient map known as the Windows Registry. This map determined which components could be used by which software. When this structure was disrupted, the system would lose its balance, and the installation of new software required administrator permissions.
The Windows Registry is a database consisting of software and hardware information, settings, options, and other values, present in all versions of the Microsoft Windows operating system.
.NET Framework: The Foundations of Traditional Power
By 1995, Microsoft engineers recognized the challenges posed by COM and its dependency on the Registry. Consequently, the NGWS (Next Generation Windows Services) project was initiated. The project was led by Anders Hejlsberg, the creator of the C# language, Jean Paoli, one of the architects of the XML standard, and Don Box, one of the creators of SOAP and XML Schemas.
NGWS — Next Generation Windows Services, was used to describe Microsoft’s plans for producing an “Internet-based Next Generation Windows Services platform” before the official announcement of .NET.
In 2002, a new era began with the introduction of the .NET Framework. This platform offered developers the opportunity to escape the complexities of COM and provided a more reliable working environment. By providing a managed code-based development environment, it created a type of code exempt from low-level operations and active memory management.
This visual was made by me using Visme.
The Evolution of .NET Framework
The .NET Framework was released by Microsoft in 2002 and quickly became a revolutionary tool for software development, especially on the Windows platform. The initial release provided developers with a powerful set of libraries and tools, making it easier to develop desktop applications using Windows Forms (WinForms).
In 2006, .NET Framework 3.0 was released, introducing technologies such as Windows Presentation Foundation (WPF), Windows Communication Foundation (WCF), Windows Workflow Foundation (WF), and Windows CardSpace. This version brought significant advancements in user interface design and application communication.
Windows Presentation Foundation (WPF) is a development framework used to create desktop applications.
Windows CardSpace is a software client introduced in Microsoft .NET Framework version 3.0, designed to allow users to provide their digital identities to online services in a simple, secure, and reliable manner.
In 2010, .NET Framework 4.0 was released with significant innovations such as the Task Parallel Library (TPL) and Entity Framework. This version marked an important step forward in terms of performance improvements and language support (C# and VB.NET).
The Task Parallel Library (TPL) is a set of classes and APIs provided by the .NET Framework to simplify the process of writing parallel and asynchronous code.
Entity Framework Core (EF Core) is an open-source object-relational mapping (ORM) framework developed by Microsoft.
This visual was made by me using Visme.
In 2012, .NET Framework 4.5 was released, making asynchronous programming easier with the introduction of the async and await keywords. It also brought numerous improvements to existing technologies such as WPF and WCF.
The latest version of the .NET Framework, 4.8.1, has received various improvements and updates. However, the future of .NET Framework is now focused on .NET 5 and beyond. While .NET Framework 4.8.1 continues to support compatibility with legacy applications, it is recommended to use .NET 5+ versions for new projects.
This visual was made by me using Visme.
.NET Framework Components
The .NET Framework consists of two main components:
1. CLR (Common Language Runtime): A virtual machine that runs on Microsoft’s .NET platforms like .NET Framework and .NET Core. CLR includes the following components:
- JIT Compiler (Just-In-Time Compiler)
- Memory Manager
- Garbage Collector (GC)
- Common Language Specification (CLS)
- Common Type System (CTS)
- Security Manager
- Exception Manager
This visual was made by me using Visme.
2. BCL (Base Class Library): The BCL is a subset of the Framework Class Library (FCL). The class library is a collection of reusable types closely integrated with the CLR. The Base Class Library provides classes and types that assist in everyday tasks, such as working with strings and primitive types, database connections, and I/O operations.
.NET Concepts:
These explanations apply to both .NET Framework and .NET Core because CLR (Common Language Runtime) and CLI (Common Language Infrastructure) are fundamental components of both platforms. However, there may be differences in the specific features of each platform.
Common Language Specification (CLS): A set of syntactical rules that ensures different programming languages can work together on the .NET platform. The CLS includes common language specifications set by the CLR for all .NET-supported languages. This allows each language compiler to produce MSIL (Microsoft Intermediate Language) code that adheres to these rules, enabling code written in different languages to work seamlessly together.
IL/Intermediate Language/MSIL (Intermediate Language): IL is a CPU-independent, partially compiled code. This intermediate language provides cross-platform portability and cannot be executed directly by the operating system.
Common Type System (CTS): A system that standardizes how data types are defined and used across languages on the .NET platform. CTS ensures that all languages work with the same types.
Just-In-Time Compiler (JIT): The JIT compiler converts IL code into native machine code immediately before execution, making the code executable directly by the system's hardware.
Garbage Collector (GC): The GC detects unused objects in .NET applications and automatically reclaims their memory, optimizing memory management.
CLI (Common Language Infrastructure): An open specification developed by Microsoft that enables code execution across different platforms. It provides standards for distribution, versioning, and security. There are two types of CLI in .NET:
- ** .EXE (Executable File):** A file that can be run directly as an application.
- ** .DLL (Dynamic Link Library):** Code libraries that can be used by other applications. CLI compilations contain code in CIL (Common Intermediate Language), and as specified, CLI programming languages compile source code into CIL code rather than platform- or processor-specific object code.
Languages and Common Language Infrastructure (CLI):
The .NET platform supports a variety of programming languages. These languages operate according to a software specification called CLI (Common Language Infrastructure). CLI provides a set of rules and structures that enable high-level languages to work across different platforms without modification. For example, code written in C# can interact seamlessly with components written in another CLI-compliant language.
Assemblies and Program Execution:
.NET compilers produce files called "Assemblies." These files contain all the code, data, and resources used in .NET applications. Assemblies come in two main types:
Assemblies are files that contain managed code and control the execution of the program. These files include all the code, metadata, and resources of the application and ensure that all these components work together.
Garbage Collector (GC) and Memory Management:
Memory management is one of the strongest aspects of the .NET Framework. CLR includes a "Garbage Collector" (GC) that automatically detects and reclaims memory from unused objects. This reduces the need for developers to handle memory leaks and performance issues. The GC monitors the lifecycle of objects and cleans up only those that are no longer accessible, making memory management more efficient.
.NET Framework Code Execution:
Code is compiled into a special language called Intermediate Language (IL) and stored in assembly files with .dll or .exe extensions. CLR converts the code into machine code (native code) when the application runs. The .NET Framework provides many services such as memory management, a common type system (Common Type System), and interoperability of CLS-compliant languages.
This visual was made by me using Visme.
Unmanaged Code
An unmanaged module consists of components of a .NET application that are not compiled into IL (Intermediate Language). This type of code runs directly as machine code and can access memory and system resources directly. It is typically used for code developed outside of .NET, such as legacy code or third-party libraries, and does not benefit from the management features provided by .NET.
Managed Code
A managed module is a component of a .NET application that is compiled into IL and managed by the CLR (Common Language Runtime). These modules are output as .dll or .exe files and benefit from .NET features such as memory management, debugging, and security, which ensures that the code runs more safely and efficiently.
Modern Development Principles and the Birth of .NET Core
2014 was a turning point for Microsoft. Satya Nadella's appointment as CEO marked a fundamental shift in the company's strategy. Nadella aimed to transform Microsoft from a software development company into a mobile and cloud-focused service provider. This vision also radically changed Microsoft's approach to the open-source world and laid the groundwork for the birth of .NET Core in 2015.
.NET Core offered a flexible, open-source, and cross-platform solution, running not only on Windows but also on operating systems like Linux and macOS. Previously, .NET Framework users were tightly bound to the Windows ecosystem. For example, Windows Communication Foundation (WCF), which was a closed system running only on Windows, posed significant challenges for developers trying to integrate with other platforms. However, with .NET Core, Microsoft began adopting more open standards, such as gRPC, which is widely used in the industry and developed by Google.
Windows Communication Foundation (WCF): Introduced with .NET Framework 3.0, WCF is a library designed to serve as the communication layer for applications written with .NET Framework.
gRPC (Google Remote Procedure Call): An open-source RPC framework used to create scalable and fast APIs. It facilitates communication in client-server relationships by allowing service methods to be used as if they were client methods, making interactions easy and quick.
You can find more detailed information here.
This transformation provided broad compatibility not only within the Microsoft ecosystem but also globally. Now, a gRPC client written in C# can communicate seamlessly with a gRPC server written in Java or JavaScript. Microsoft's shift has broken down barriers in the software world, offering developers the freedom to work within a more expansive ecosystem.
Annual Release Cycle and Support Strategies
With .NET 5, Microsoft adopted a new annual release cycle, aiming to release a new .NET version every November. This new system provides developers with a more consistent and predictable development process. The release cycle progresses through .NET 5, .NET 6, .NET 7, and .NET 8.
With these annual releases, Microsoft has different support strategies:
STS (Standard Term Support): These versions (odd-numbered releases like .NET 5, 7, etc.) provide short-term support, typically for 18 months from the release of the next version. If you want to integrate the latest features into your project, you can use STS versions.
LTS (Long-Term Support): These versions (even-numbered releases like .NET 6, 8, etc.) offer long-term support for three years. If you are developing a more stable and long-term project, LTS versions are recommended as they provide a reliable foundation over the long term.
LTS and STS are relevant only for .NET 5+ versions, and support policies for other versions will depend on the .NET platform you are using.
.NET and .NET Core Support Policy.
.NET Core CLR: A New Way to Handle Different Code
One of the most significant innovations brought by .NET Core is the ability to run the same code on different runtimes (CLR). This innovation applies not only to C# but also to other languages within the .NET platform. Therefore, this change introduced with .NET Core is a critical point in understanding the workings of .NET Framework and .NET Core.
Behind the Scenes: Executing Your Code
To understand how .NET Framework and .NET Core work, it is important to know how code is processed behind the scenes. For example, let's consider a simple “Hello World” application written in C#:
Compilation from Source Code to Intermediate Language (IL): Suppose you have written a simple “Hello World” application. When you compile this application using .NET Framework or .NET Core, your C# code is converted into an Intermediate Language (IL).
IL is the common language for all languages within the .NET ecosystem and allows different languages to work on the same platform.
The key point here is that the same or similar code can run on both .NET Framework and .NET Core. This flexibility is one of the main motivations behind the development of .NET Core. Microsoft realized that it was possible to write a different CLR, which led to faster and more efficient code execution.
This visual was made by me using Visme.
Execution with Common Language Runtime (CLR): The compiled IL code is executed in a runtime environment known as the CLR. The CLR converts IL code into machine code (native code) using the Just-In-Time (JIT) compiler and manages memory with Garbage Collection. This process happens similarly whether the “Hello World” code is run on .NET Framework or .NET Core CLR, ensuring that your application runs smoothly.
Platform Dependence: During the time of .NET Framework, the CLR was directly dependent on the Windows operating system. However, Microsoft overcame this limitation by developing CoreCLR, a platform-independent CLR. This allows you to run your code on different platforms such as Windows, Linux, and macOS.
This visual was made by me using Visme.
.NET Core and .NET 5+: Evolution and Future
Git and Version Control Systems
For many years, Microsoft promoted its own version control system, Team Foundation Server (TFS). However, over time, the software development world began to focus on Git. Recognizing this trend, Microsoft shifted its strategy and began comprehensive support for Git. This transformation was not only a technological adaptation but also reflected how Microsoft was interacting more effectively with the community.
Team Foundation Server (Microsoft TFS) provides tools and technologies to help teams collaborate better and manage their projects. Microsoft TFS offers a combination of version control, issue tracking, and application lifecycle management.
With Microsoft’s acquisition of GitHub on June 4, 2018, it significantly contributed to the Git ecosystem and developed tools that integrate with Git. This change demonstrates how Microsoft has adapted to modern software development processes and strengthened its connections with the community.
Ease of Upgrades: Flexibility with .NET Core
One of the advantages of .NET Core is avoiding the difficult upgrade processes experienced in the past. The transition from .NET Framework 3.5 to 4 was a painful process for many developers. During this period, Microsoft's major changes led to compatibility issues and caused headaches for many projects. Considerable effort was put into minimizing such problems with .NET Core, making it a platform that can be continuously updated and managed more easily.
If your project works with .NET 5 and you want to upgrade to .NET 7, generally, the only thing you need to do is update your NuGet packages. By simplifying the upgrade process, developers can spend more time focusing on developing innovative solutions.
Of course, there are specific risks with each upgrade. Especially if you have written custom code, you need to assess how that code behaves in the new version. However, in .NET Core, such breaking changes are usually minimal and often manageable.
This visual was made by me using Visme.
.NET Sibling Showdown: The Final Stop - .NET (Modern)
With the release of .NET Framework 4.8 and .NET Core 3.1 in December 2019, it became evident that .NET Core had become comparable to .NET Framework in terms of features. However, to resolve the confusion arising from version numbers, Microsoft renamed .NET Core directly to .NET 5.0. This new naming convention was implemented to avoid confusion with .NET Framework 4.x. .NET 5 is simply referred to as ".NET," marking its separation from .NET Core.
.NET Standard: The Universal Language of Code
In the .NET world, cross-platform communication had become a significant issue over time. Platforms such as .NET Framework, Mono, Xamarin, and .NET Core each operated with their own libraries and standards. This situation can be likened to an automobile manufacturer planning to sell in various countries and having to redesign cars to meet each country's standards. Developing a model specific to each country complicated production and increased costs.
Similarly, developers faced significant challenges when moving code from one platform to another, and ensuring that the same code would work across different platforms seemed nearly impossible. To address this complexity and ensure compatibility across platforms, Microsoft sought a solution. This effort led to the creation of .NET Standard.
.NET Standard is a standard that allows you to write your code once and use it across many different platforms. It provides great flexibility and efficiency in the software development process. Each version of .NET Standard offers a list of supported APIs and types, which determines which APIs are supported on which platforms. .NET Standard serves as a guide developed to ensure compatibility across the various platforms within the .NET ecosystem.
This visual was made by me using Visme.
.NET Standard: The Key to Cross-Platform Compatibility
One of the greatest advantages provided by .NET Standard is the ability to target new platforms without the need to recompile your libraries. This means that you do not need to wait for the authors of the libraries you depend on to recompile their libraries as well. Additionally, it eliminates confusion about which APIs are available — the higher the .NET Standard version you target, the more APIs you have access to.
Note: Just because a platform supports a specific .NET Standard version does not mean that all methods will work seamlessly on that platform. For example, some reflection APIs may not be available on every platform. .NET 7 includes Roslyn Analyzer support to detect such issues and warns you during compilation. For more information, I recommend reading the article "Automatically find latent bugs in your code with .NET 5" on the .NET Blog.
.NET Standard 2.0: Compatibility Challenges and Microsoft’s Tough Decision
Although .NET Standard 2.0 is a complete superset of .NET Standard 1.6, applications targeting .NET Framework 4.6.1 can reference .NET Standard 2.0 libraries, but technically only support .NET Standard 1.4.
Note: Despite the fact that .NET Framework 4.6.1 technically only supports .NET Standard 1.4, it can reference .NET Standard 2.0 libraries. This is a special case that applies only to versions 4.6.1–4.7.0. .NET Framework 4.7.1, which supports .NET Standard 2.0, can naturally reference .NET Standard 2.0 libraries.
The “Chicken-and-Egg” Problem: Microsoft’s Tough Choice
The logic behind this decision was to address the “chicken-and-egg” problem faced by software developers. One early criticism of .NET Core 1.x was its lack of existing APIs, making it difficult to migrate projects to .NET Core. Taking this criticism into account, Microsoft added thousands of APIs present in the most commonly used .NET Framework version, .NET Framework 4.6.1, to .NET Standard 2.0 with the release of .NET Core 2.0. The goal was to ensure that .NET Standard 2.0 would provide the same APIs as .NET Framework 4.6.1.
Tip: The rationale behind this step is detailed in the article “Introducing .NET Standard” on the .NET Blog. I recommend reading it.
However, .NET Framework 4.6.1 unfortunately does not include APIs from .NET Standard 1.5 or 1.6. Since .NET Standard 2.0 is a complete superset of .NET Standard 1.6, .NET Framework 4.6.1 cannot fully support .NET Standard 2.0. This complexity put Microsoft in a difficult position. If the most popular .NET Framework version did not support .NET Standard 2.0, developers would not write .NET Standard 2.0 libraries, which would hinder the adoption of .NET Core 2.0. Consequently, Microsoft decided to allow .NET Framework 4.6.1 to reference .NET Standard 2.0 libraries.
A graphic showing that .NET Framework 4.6.1 does not fully contain .NET Standard 1.5, 1.6, and 2.0 APIs might effectively explain the complexity of this situation and why Microsoft sought such a solution. Thus, while .NET Framework 4.6.1 can reference .NET Standard 2.0 libraries, these libraries are not technically fully supported. To address this issue, the .NET Core 2.0 SDK must be installed.
This visual was made by me using Visme.
.NET Standard and Transition Strategies
The differences between .NET Framework, .NET Standard, and .NET Core and the strategies for transitioning between them play a crucial role in modern software development. Here are the details of these concepts and considerations for the transition process:
- Objective: To provide a common API set across various .NET platforms.
- Versions and Support:
.NET Standard 1.0: Approximately 7,949 APIs, compatible with .NET Framework 4.5 and .NET Core 1.0.
.NET Standard 2.0: Approximately 32,000 APIs, compatible with .NET Framework 4.6.1 and .NET Core 2.0.
.NET Standard 2.1: Approximately 37,118 APIs, compatible only with .NET Core 3.0 and later; not compatible with .NET Framework.
- Features: Increases code portability and compatibility. Newer versions include APIs from previous versions and offer additional APIs.
Transition Process and Strategies
Modularizing Code:
- Step: Separate your code into class libraries, isolating business logic from the user interface (UI).
- Benefit: This approach increases code reusability and creates modular structures, making maintenance and expansion easier.
Migrating Class Library to .NET Standard:
- Step: Make your existing .NET Framework class library compatible with .NET Standard.
- Benefit: This change allows your library to be used on both .NET Framework and .NET Core platforms, enhancing code portability.
Transition Process:
- Using .NET Standard 2.0: When transitioning from .NET Framework projects to .NET Core, it is generally recommended to use .NET Standard 2.0. This version offers the broadest API support and facilitates the transition process.
- Version Upgrades: When upgrading your .NET Core class library, you can take advantage of new features in later versions. This helps your project stay aligned with current technologies.
Practical Examples and Benefits
Transitioning:
- Example: If you are using WinForms or WPF in a .NET Framework application and want to transition to .NET Core, making your library compatible with .NET Standard simplifies the transition process.
Library Transformation:
- Example: Converting a .NET Framework class library to a .NET Standard class library allows the library to be used on both old and new platforms, extending its usability.
Version Upgrades:
- Example: Upgrading your .NET Standard class library to .NET Core allows you to leverage the latest features and performance improvements.
Advantages:
- Code Portability: .NET Standard enhances code portability by providing a common API set across different .NET platforms, ensuring smooth operation across multiple platforms.
- Flexibility: You can run your code on both .NET Framework and .NET Core, maintaining compatibility during the transition process. This flexibility prepares your projects for future updates and changes.
The Beginning and Evolution of Mono
Renowned engineer Miguel de Icaza, who made a significant impact on the Linux community with his GNOME desktop environment projects, and Federico Mena, launched the Mono Project in 2004. Mono aimed to provide a cross-platform reimplementation of the .NET Framework on Linux. This effort was not only about .NET Framework but also about developing an open-source implementation of the Common Language Infrastructure (CLI).
GNOME (GNU Network Object Model Environment): A graphical user interface (GUI) and desktop application suite for Linux operating system (OS) users.
Common Language Infrastructure (CLI): A toolchain that runs your compiled code known as Intermediate Language (IL). CLI includes several essential components in software development:
Common Type System (CTS): Related to Base Class Libraries (BCL) and ensures compatibility of data types used in different programming languages.
Common Language Specification (CLS): Contains metadata about your code and guarantees compatibility between .NET languages.
Virtual Execution System (VES): Executes and runs your compiled application code at runtime.
This structure is standardized by both ISO and ECMA (ISO/IEC 23271:2012 and ECMA-335). CLI’s adherence to these standards ensures compliance and portability in software development processes.
The initial release of Mono filled a significant gap by providing full support for C# 1.0. Shortly thereafter, version 1.1 was released with support for C# 1.1. Mono achieved full compatibility with the .NET Framework in just two years. During this period, the Mono team developed their own APIs, such as Mono.Cecil and Mono.Cairo, to strengthen the project. With the introduction of Mono 2.2 in 2009, static compilation, which allows projects to be compiled into native code, was added.
The Xamarin Era
In 2011, with Novell’s acquisition by Attachmate, Mono’s future became uncertain. Employees were laid off, and much speculation arose about the project's future. However, Miguel de Icaza did not give up. He founded a new company called Xamarin, securing Mono’s future. Xamarin provided tools for developing iOS and Android applications using Mono, gaining significant traction in the mobile platform space.
In 2012, Xamarin released Xamarin Studio with an extension for MonoDevelop and integration with Visual Studio. In 2016, with Xamarin’s acquisition by Microsoft, Xamarin became part of Visual Studio, and Mono was re-licensed under the MIT license. These steps helped Mono reach a broader developer audience.
This visual was made by me using Visme.
Mono and .NET Core
The differences between Mono and .NET Core raise some critical questions. Mono was initially designed to be compatible with the .NET Framework, but .NET Core’s modular structure ensures that only the necessary dependencies are included. This means that .NET Core requires a smaller installation and less disk space. Those who maintain legacy servers are well aware of the challenges posed by installing various versions of the .NET Framework.
Although these issues have diminished in the cloud era, the use of Mono is inevitable with tools like Blazor, Xamarin, and Unity 3D. Therefore, understanding how Mono works can provide a significant advantage in your software development processes.
The MAUI Era
A significant milestone in the evolution of Xamarin was MAUI (Multi-platform App UI) introduced with .NET 6. MAUI replaces Xamarin.Forms, enabling application development for iOS, Android, macOS, and Windows with a single codebase. MAUI offers a simpler structure, helping developers to build applications faster and more efficiently.
One of the major innovations brought by MAUI is the provision of more comprehensive and flexible tools for modern interface design. Additionally, it includes numerous new features that simplify and accelerate the application development process. This allows developers to enhance application performance and user experience.
Future Perspectives on .NET Technologies
.NET Framework: Version 4.8 will continue to receive long-term support. However, this support will be indefinite based on the operating system it is installed on, and no new features or improvements will be added to the .NET Framework. Innovations and enhancements will focus on .NET Core and later versions. Support for .NET Framework 3.5 will continue until April 2029.
.NET Core: Each new version of .NET Core typically receives long-term support for three years. During this period, Microsoft provides updates and new features, offering developers flexibility to stay up-to-date with current technologies. Various versions of .NET Core exist from 1.0 to 3.1. However, support for .NET Core 3.1 ended on December 13, 2022.
.NET Standard: .NET Standard served as a compatibility bridge between .NET Framework and .NET 5+. The latest version, 2.1, continues to be supported with .NET 5+ and later versions.
Xamarin: Xamarin revolutionized cross-platform mobile application development using C# code sharing. However, support for this technology will end on May 1, 2024. Xamarin users are encouraged to migrate their projects to .NET MAUI.
.NET MAUI: (Multi-platform App UI) is a cross-platform application development framework developed to replace Xamarin. MAUI allows you to develop applications for Android, iOS, macOS, and Windows with a single codebase. Introduced with .NET 6, .NET MAUI is a cornerstone of Microsoft’s future mobile and desktop application development strategy.
.NET 5+: Microsoft introduced the latest series of .NET versions starting with .NET 5 in 2020. This new series features an accelerated release cadence and a support policy aligned with this pace.
The End of a Journey, New Beginnings
The evolution of the .NET ecosystem presents a constantly changing landscape in the software world. In this article, we detailed the journey from the rich history of the .NET Framework to the innovative structure of .NET Core and the current .NET 5+. We also highlighted how MAUI facilitates the transition from Xamarin and the flexibility and compatibility it offers developers, making software development processes more efficient.
If you enjoyed this article, you can read my first post on C# here, where I covered basic concepts and the starting points of this journey. Both articles can help you gain in-depth knowledge in the software world.
Wishing you good luck and success in your software development journey! 😇🐣
You can also connect with me on other platforms:
Top comments (4)
Excellent run down Thanks.
Thank you! I’m glad you enjoyed it. Your feedback is much appreciated!
Thank you so much! I'm really glad you found it helpful. I'll keep working on content that you can come back to. Your feedback means a lot to me :)