Schlagwort: C#

  • Simple Data Seeding with Entity Framework my preferred way

    Simple Data Seeding with Entity Framework my preferred way

    A common challenge in developing database solutions is the insertion and updating of master data. By master data, I mean information such as titles („Mr.“, „Ms.“, „Prof.“, etc.), product categories („Food“, „Tools“, „Services“), or the list of all countries in the world – useful, for example, in address capturing.

    Staying up-to-date with master data without much effort

    How can you get this data into the database and keep it current without a high level of effort?

    In this example, I will show you how all the countries of the world with their official abbreviation codes are automatically entered into a table when creating the database — either during the initial creation or during a migration of the database. You will learn how to further simplify the work involved in database creation and migrations in another blog post.

    Using Entity Framework for easy master data collection

    Fortunately, Entity Framework provides helpful functions, which I would like to demonstrate through my code.

    For this example, I have stored a list of all countries in a class with constants. I generated the list of all countries and their abbreviations using GitHub Copilot — so if there are any errors, please blame Copilot, not me 😉

    public static class Countries
    {
        public static Dictionary<string, string> All = new Dictionary<string, string>
            {
                { "Afghanistan", "AF" },
                { "Albania", "AL" },
                { "Algeria", "DZ" },
                { "Andorra", "AD" },
                { "Angola", "AO" },
                { "Antigua and Barbuda", "AG" },
                { "Argentina", "AR" },
                { "Armenia", "AM" },
                { "Australia", "AU" },
                { "Austria", "AT" },
                { "Azerbaijan", "AZ" },
                { "Bahamas", "BS" },
                { "Bahrain", "BH" },
                { "Bangladesh", "BD" },
                { "Barbados", "BB" },
                { "Belarus", "BY" },
                { "Belgium", "BE" },
                { "Belize", "BZ" },
                { "Benin", "BJ" },
                { "Bhutan", "BT" },
                { "Bolivia", "BO" },
                { "Bosnia and Herzegovina", "BA" },
                { "Botswana", "BW" },
                { "Brazil", "BR" },
                { "Brunei", "BN" },
                { "Bulgaria", "BG" },
                { "Burkina Faso", "BF" },
                { "Burundi", "BI" },
                { "Cabo Verde", "CV" },
                { "Cambodia", "KH" },
                { "Cameroon", "CM" },
                { "Canada", "CA" },
                { "Central African Republic", "CF" },
                { "Chad", "TD" },
                { "Chile", "CL" },
                { "China", "CN" },
                { "Colombia", "CO" },
                { "Comoros", "KM" },
                { "Congo, Democratic Republic of the", "CD" },
                { "Congo, Republic of the", "CG" },
                { "Costa Rica", "CR" },
                { "Croatia", "HR" },
                { "Cuba", "CU" },
                { "Cyprus", "CY" },
                { "Czech Republic", "CZ" },
                { "Denmark", "DK" },
                { "Djibouti", "DJ" },
                { "Dominica", "DM" },
                { "Dominican Republic", "DO" },
                { "Ecuador", "EC" },
                { "Egypt", "EG" },
                { "El Salvador", "SV" },
                { "Equatorial Guinea", "GQ" },
                { "Eritrea", "ER" },
                { "Estonia", "EE" },
                { "Eswatini", "SZ" },
                { "Ethiopia", "ET" },
                { "Fiji", "FJ" },
                { "Finland", "FI" },
                { "France", "FR" },
                { "Gabon", "GA" },
                { "Gambia", "GM" },
                { "Georgia", "GE" },
                { "Germany", "DE" },
                { "Ghana", "GH" },
                { "Greece", "GR" },
                { "Grenada", "GD" },
                { "Guatemala", "GT" },
                { "Guinea", "GN" },
                { "Guinea-Bissau", "GW" },
                { "Guyana", "GY" },
                { "Haiti", "HT" },
                { "Honduras", "HN" },
                { "Hungary", "HU" },
                { "Iceland", "IS" },
                { "India", "IN" },
                { "Indonesia", "ID" },
                { "Iran", "IR" },
                { "Iraq", "IQ" },
                { "Ireland", "IE" },
                { "Israel", "IL" },
                { "Italy", "IT" },
                { "Jamaica", "JM" },
                { "Japan", "JP" },
                { "Jordan", "JO" },
                { "Kazakhstan", "KZ" },
                { "Kenya", "KE" },
                { "Kiribati", "KI" },
                { "Korea, North", "KP" },
                { "Korea, South", "KR" },
                { "Kosovo", "XK" },
                { "Kuwait", "KW" },
                { "Kyrgyzstan", "KG" },
                { "Laos", "LA" },
                { "Latvia", "LV" },
                { "Lebanon", "LB" },
                { "Lesotho", "LS" },
                { "Liberia", "LR" },
                { "Libya", "LY" },
                { "Liechtenstein", "LI" },
                { "Lithuania", "LT" },
                { "Luxembourg", "LU" },
                { "Madagascar", "MG" },
                { "Malawi", "MW" },
                { "Malaysia", "MY" },
                { "Maldives", "MV" },
                { "Mali", "ML" },
                { "Malta", "MT" },
                { "Marshall Islands", "MH" },
                { "Mauritania", "MR" },
                { "Mauritius", "MU" },
                { "Mexico", "MX" },
                { "Micronesia", "FM" },
                { "Moldova", "MD" },
                { "Monaco", "MC" },
                { "Mongolia", "MN" },
                { "Montenegro", "ME" },
                { "Morocco", "MA" },
                { "Mozambique", "MZ" },
                { "Myanmar", "MM" },
                { "Namibia", "NA" },
                { "Nauru", "NR" },
                { "Nepal", "NP" },
                { "Netherlands", "NL" },
                { "New Zealand", "NZ" },
                { "Nicaragua", "NI" },
                { "Niger", "NE" },
                { "Nigeria", "NG" },
                { "North Macedonia", "MK" },
                { "Norway", "NO" },
                { "Oman", "OM" },
                { "Pakistan", "PK" },
                { "Palau", "PW" },
                { "Palestine", "PS" },
                { "Panama", "PA" },
                { "Papua New Guinea", "PG" },
                { "Paraguay", "PY" },
                { "Peru", "PE" },
                { "Philippines", "PH" },
                { "Poland", "PL" },
                { "Portugal", "PT" },
                { "Qatar", "QA" },
                { "Romania", "RO" },
                { "Russia", "RU" },
                { "Rwanda", "RW" },
                { "Saint Kitts and Nevis", "KN" },
                { "Saint Lucia", "LC" },
                { "Saint Vincent and the Grenadines", "VC" },
                { "Samoa", "WS" },
                { "San Marino", "SM" },
                { "Sao Tome and Principe", "ST" },
                { "Saudi Arabia", "SA" },
                { "Senegal", "SN" },
                { "Serbia", "RS" },
                { "Seychelles", "SC" },
                { "Sierra Leone", "SL" },
                { "Singapore", "SG" },
                { "Slovakia", "SK" },
                { "Slovenia", "SI" },
                { "Solomon Islands", "SB" },
                { "Somalia", "SO" },
                { "South Africa", "ZA" },
                { "South Sudan", "SS" },
                { "Spain", "ES" },
                { "Sri Lanka", "LK" },
                { "Sudan", "SD" },
                { "Suriname", "SR" },
                { "Sweden", "SE" },
                { "Switzerland", "CH" },
                { "Syria", "SY" },
                { "Taiwan", "TW" },
                { "Tajikistan", "TJ" },
                { "Tanzania", "TZ" },
                { "Thailand", "TH" },
                { "Timor-Leste", "TL" },
                { "Togo", "TG" },
                { "Tonga", "TO" },
                { "Trinidad and Tobago", "TT" },
                { "Tunisia", "TN" },
                { "Turkey", "TR" },
                { "Turkmenistan", "TM" },
                { "Tuvalu", "TV" },
                { "Uganda", "UG" },
                { "Ukraine", "UA" },
                { "United Arab Emirates", "AE" },
                { "United Kingdom", "GB" },
                { "United States", "US" },
                { "Uruguay", "UY" },
                { "Uzbekistan", "UZ" },
                { "Vanuatu", "VU" },
                { "Vatican City", "VA" },
                { "Venezuela", "VE" },
                { "Vietnam", "VN" },
                { "Yemen", "YE" },
                { "Zambia", "ZM" },
                { "Zimbabwe", "ZW" }
            };
    }

    My DbContext looks as follows:

    pulic class AdminDbContext : DbContext
    {
        public AdminDbContext(DbContextOptions<AdminDbContext> options)
            : base(options)
        {
        }
    
        public DbSet<GroupEntity> Groups { get; init; } = default!;
    
        public DbSet<MemberEntity> Members { get; init; } = default!;
        
        public DbSet<CountryEntity> Countries { get; init; } = default!;
    
    }

    My DbContext looks as follows:

    
    [EntityTypeConfiguration(typeof(CountryEntityConfiguration))]
    public class CountryEntity 
    {
        [Key]
        [StringLength(2)]
        public string Id { get; set; } = default!;
        public string Name { get; set; } = default!;
    }

    This entity consists of only two properties, namely the Id and the name of the country. The Id key is simultaneously the Country Code and must never be longer than two characters. The most interesting part of this class lies in the attribute EntityTypeConfiguration. This attribute accepts another class as its type: CountryEntityConfiguration.

    In the file CountryEntityConfiguration, the entity is described in detail. This combination does exactly what has previously been done in the OnModelCreating event in the DbContext.

    public class CountryEntityConfiguration: IEntityTypeConfiguration<CountryEntity>
    {
        public void Configure(EntityTypeBuilder<CountryEntity> builder)
        {
            string tableName = "Countries";
            builder.ToTable(tableName, "dbo");
    
            // Unique Index
            // No duplicate Country Name allowed
            builder
                .HasIndex(c => c.Name)
                .IsUnique();
    
            // Seeding All Countries
            List<CountryEntity> countryEntities = [];
            foreach (var country in Countries.All)
            {
                CountryEntity countryEntity = new()
                {
                    Id = country.Value,
                    Name = country.Key
                };
                countryEntities.Add(countryEntity);
            }
            
            builder.HasData(
                countryEntities
            );
        }
    }

    By using this, if you create a migration or regenerate the database using DbContext.Database.EnsureCreated(), you will get a populated table with all the countries of the world.

    I hope you learned something new, happy auto-fill your database with entity framework.

  • Start with ASP.NET Core, Blazor and Fluent UI

    Start with ASP.NET Core, Blazor and Fluent UI

    I’ve been away from real UI projects for a while, because of focusing on Azure backend things in the last projects. But recently, I needed to create some simple UIs for several projects to pump data into databases. While almost everyone at Medialesson loves Angular, I wanted to explore something different and revisit my roots. That’s why I chose Blazor as my „new“ UI framework of the month. I was surprised at how easy it is to get started with it.

    Another important fact about developer is, they are developers and not designers. That is also true for me. Years ago, I realized that I’m not particularly talented at building nice UIs, so I wanted to keep the design simple and use existing controls and themes. I searched a while and I’m happy that I discovered that Fluent is quite easy to use for decent design, and it brings a lot of good UI controls, like my favorite data grid.

    In this post, I want to lay out the base for my upcoming posts about how to build data-driven apps with Blazor and Fluent.

    Agenda

    – Why I Like Blazor and Fluent?
    – What is Blazor?
    – What is Fluent 2?
    – The First Application

    Why I Like Blazor and Fluent

    My top (incomplete and still growing) highlights in Blazor are:
    – Pure C#, with no real need for JavaScript/TypeScript, though it’s possible to use them.
    – Real components that can be structured in libraries for reuse.
    – Reuse of almost any other C#/.NET features, like Entity Framework and Dependency Injection.
    – Older code still works seamlessly.
    – Controls, controls, and even more controls.

    But before I begin coding in the next posts, I want to highlight some essentials about Blazor and the Fluent design.

    What is Blazor?

    Blazor is a …
    Web Framework: Blazor is a web framework developed by Microsoft that allows developers to build interactive web applications using C# instead of JavaScript.

    And it brings …
    .NET Integration: It is part of the ASP.NET Core framework, enabling full-stack web development with .NET, sharing code between server and client.
    Web Assembly Support: Blazor Web Assembly (WASM) runs client-side in the browser via Web Assembly, allowing for near-native performance and offline capabilities.
    Component-Based Architecture: Blazor uses a component-based architecture, where UI components are built as reusable pieces of code that can include markup and logic.
    SignalR Integration: Blazor Server uses SignalR for real-time web functionality, maintaining a constant connection between the client and server to handle user interactions and UI updates.

    More information about Blazor: https://blazor.net/

    What is Fluent 2?

    Fluent is a …
    Design System: Microsoft Fluent 2 is a design system that provides a comprehensive set of design guidelines, components, and tools to create cohesive, accessible, and high-quality user interfaces.

    And it brings …
    Cross-Platform: Fluent 2 is designed to work across multiple platforms, including web, mobile, and desktop, ensuring a consistent user experience across different devices and applications.
    Modern Aesthetics: It focuses on modern design principles such as simplicity, clarity, and efficiency, with an emphasis on clean lines, intuitive layouts, and vibrant yet harmonious color schemes.
    Accessibility: Fluent 2 prioritizes accessibility, providing guidelines and components that help developers create inclusive applications that are usable by people with various disabilities.
    Customization and Flexibility: The system is highly customizable, allowing developers to tailor the design components to match their brand identity while maintaining a coherent overall look and feel.

    More information about Fluent 2: https://blazor.net/

    Getting Started

    I work with the latest version of the .NET Core SDK: https://dotnet.microsoft.com/en-us/download.

    I always use a mix of [Visual Studio](https://visualstudio.microsoft.com/en/downloads/) and [Visual Studio Code](https://code.visualstudio.com/) for editing code. Visual Studio Code is more straightforward and shows all files, while Visual Studio has a richer editing UI but hides some of the dirty secrets.

    I assume you have the Web Development package installed when using Visual Studio.

    The Fluent UI features are not part of the default installation of Visual Studio or the .NET SDKs. They are maintained separately on GitHub: https://github.com/microsoft/fluentui-blazor. Fortunately, there are project templates for `dotnet`, which can be used with the dotnet CLI and/or Visual Studio.

    You can also manually add them to existing projects with the package manager.

    dotnet add package Microsoft.Fast.Components.FluentUI
    

    But honestly, you need to add some more files, links, etc., to your project. The complete documentation on how to add Fluent to an existing Blazor app can be found here.

    For now, I prefer to start fresh on a greenfield project.

    To check if you have the project templates already installed:

    # list installed templates
    dotnet new list
    

    If you can’t find them in the list, install them from the cli with:

    # install blazor fluent templates
    dotnet new install Microsoft.FluentUI.AspNetCore.Templates
    

    Create Your First Blazor Fluent App

    Create a new project with `dotnet` cli and start it with:

    dotnet new fluentblazor -n ConcertDatabase
    cd ConcertDatabase
    dotnet run
    

    Create a new project in Visual Studio:

    Hit F5 in Visual Studio or click the web-link in the cli and you will see the beautiful sample web app

    UI Controls

    When you are interest in what kind of controls come with Fluent for Blazor, take a look at: https://www.fluentui-blazor.net/. That’s where I got my inspirations and sample codes from

    I hope you like the easiness of Blazor like I do.

  • How I Taught ChatGPT to Read the Clock: Introducing Semantic Kernel

    How I Taught ChatGPT to Read the Clock: Introducing Semantic Kernel

    This article is a guide to developing your first semantic kernel app using dotnet and C#, enabling you to add dynamic features to your AI solution.

    Challenge and Problem Statement

    A common limitation of AI models is their static nature. For instance, when asked “Is the queen still alive?” ChatGPT might respond affirmatively based on outdated information. Such models struggle with dynamically changing information and complex calculations not readily available in public documents.

    What Time Is It?

    Ever wondered why ChatGPT can’t provide the current date and time? As a text-generating engine, it relies on predictions from existing data. Attempting to ask for the current date, time, or day of the week yields no response.

    Below is the initial version of my sample application with no additional plugins.

    To enhance your AI solution’s intelligence, you can leverage the plugin feature of the open-source Semantic Kernel SDK. This enables you to write your own “features” for a large language model.

    Requirements

    To create your first semantic kernel plugin, I recommend using the latest version of dotnet and Visual Studio Code.

    Additionally, you’ll need to install the Semantic Kernel SDK in your project with: dotnet add package Microsoft.SemanticKernel.

    You’ll also need an existing Azure OpenAI Service in your Azure Tenant.

    Code

    The demo application is a basic console chat application offering only rudimentary mathematical and datetime calculation functions.

    Configuration

    Create a configuration file named appsetting.json, and include your model’s name, endpoint, and key in the json.

    {
      "OpenAIEndpoint": "",
      "OpenAPIKey": "",
      "ModelName": ""
    }
    

    Plugin Code

    To write a plugin it is only required to add attributes to methods and parameters. Those attributes indicate the methods with intentions to return data.

    Create a new file with the name DateTimePlugin.cs.

    using Microsoft.SemanticKernel;
    using System.ComponentModel;
    
    namespace Oliver.AI.Samples.ChatGPTPlugin.Plugins
    {
        public sealed class DateTimePlugin
        {
            [KernelFunction, Description("What date is today?")]
            public static DateTime GetDate()
            {
                return DateTime.Today;
            }
    
            [KernelFunction, Description("What day of week is today?")]
            public static DayOfWeek GetDay()
            {
                return DateTime.Today.DayOfWeek;
            }
    
            [KernelFunction, Description("What time is it?")]
            public static DateTime GetTime()
            {
                return DateTime.Now;
            }
        }
    }
    

    The Console Application Code

    The magic to add the plugin to the existing ChatCompletionService is just s single line of code:

    builder.Plugins.AddFromType&lt;DateTimePlugin>();
    

    The complete code with the file name Program.cs.

    using Microsoft.Extensions.Configuration;
    using Microsoft.SemanticKernel;
    using Microsoft.SemanticKernel.ChatCompletion;
    using Microsoft.SemanticKernel.Connectors.OpenAI;
    using Oliver.AI.Samples.ChatGPTPlugin.Plugins;
    
    #region Configuration
    
    // Read the condiguration from an appsettings.json file
    // to avoid exploiting the API key and endpoint in a demo
    
    var configuration = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("appsettings.json", true)
        .AddJsonFile("appsettings.Development.json", true)
        .Build();
    
    string endpoint = configuration["OpenAIEndpoint"] ?? "";
    string modelName = configuration["ModelName"] ?? "";
    string apiKey = configuration["OpenAPIKey"] ?? "";
    
    #endregion 
    
    // Create kernel
    IKernelBuilder builder = Kernel.CreateBuilder();
    
    // Add a text or chat completion service using either:
    builder.Services.AddAzureOpenAIChatCompletion(modelName, endpoint, apiKey);
    builder.Plugins.AddFromType&lt;MathPlugin>();
    builder.Plugins.AddFromType&lt;DateTimePlugin>();
    
    Kernel kernel = builder.Build();
    
    // Create chat history
    ChatHistory history = [];
    
    // Get chat completion service
    var chatCompletionService = kernel.GetRequiredService&lt;IChatCompletionService>();
    
    Console.WriteLine("Olivers ChatGPT Plugins");
    Console.WriteLine("-----------------------");
    Console.WriteLine("Type 'exit' to quit the conversation");
    Console.WriteLine();
    
    // Start the conversation
    while (true)
    {
        // Get user input
        Console.Write("User > ");
        string userInput = Console.ReadLine()!;
        if (userInput.ToLower() == "exit")
        {
            break;
        }
        history.AddUserMessage(userInput);
    
        // Enable auto function calling
        OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
        {
            ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
        };
    
        // Get the response from the AI
        var result = chatCompletionService.GetStreamingChatMessageContentsAsync(
            history,
            executionSettings: openAIPromptExecutionSettings,
            kernel: kernel);
    
        // Stream the results
        string fullMessage = "";
        var first = true;
        await foreach (var content in result.ConfigureAwait(false))
        {
            if (content.Role.HasValue &amp;&amp; first)
            {
                Console.Write("Assistant > ");
                first = false;
            }
            Console.Write(content.Content);
            fullMessage += content.Content;
        }
        Console.WriteLine();
        Console.WriteLine();
    
        // Add the message from the agent to the chat history
        history.AddAssistantMessage(fullMessage);
    }
    
    Console.WriteLine("Goodbye!");
    
    

    Running the Application

    Run the application and ask some of the following questions:

    • Which day is today?
    • What time is it?
    • Which day of the week is today?
    • Welcher Wochentag ist heute?

    Video

    I’ve recorded a short video demonstrating this process, which I’ve posted on YouTube.

    English Version | German Version

    Conclusion

    With the Semantic Kernel, you can create scenarios beyond simple questions. You can retrieve data from internal sources, engage in more intensive dialogs, and much more. Stay tuned for further developments.

    You can find more information here.

  • Checking App Settings at Startup

    Checking App Settings at Startup

    This article provides a comprehensive sample demonstrating how to effectively utilize app settings in ASP.NET Core applications.

    Problem Statement

    In the realm of application development, managing settings efficiently can be a pivotal but often overlooked aspect, especially when collaborating with team members. Imagine a scenario where you or your colleagues add or remove settings during development, such as passwords, connection strings, or keys. These sensitive pieces of information should never find their way into your source code control system.

    However, a common occurrence is that someone adds a new setting essential for a feature without communicating it to other team members. Consequently, you might encounter unexpected exceptions or peculiar behavior within your application, leading to time-consuming investigations.

    Consider this familiar code snippet:

    string openAIKey = Environment.GetEnvironmentVariable("OpenAIKey");
    

    This pattern, while prevalent, is both frustrating and risky when employed within teams.

    ## Solution

    To mitigate such issues effectively, I strongly advocate for implementing the following practices:

    1. Define Settings in a Dedicated Class

    using System.ComponentModel.DataAnnotations;
    
    namespace Oliver.Tools.Copilots
    {
        public class OpenAISettings
        {
            public const string Key = "OpenAISettings";
    
            [Required(ErrorMessage = "OpenAIKey required")]
            public required string OpenAIKey { get; set; }
    
            [Required(ErrorMessage = "OpenAIEndpoint required")]
            public required string OpenAIEndpoint { get; set; }
        }
    }
    

    2. Configure Settings at Startup in Program.cs

    IServiceCollection services = builder.Services;
    
    IConfigurationSection? openAISettings = builder.Configuration.GetSection(OpenAISettings.Key);
    services
        .Configure<OpenAISettings>(openAISettings)
        .AddOptionsWithValidateOnStart<OpenAISettings>()
        .ValidateDataAnnotations();
    

    3. Run the application

    Encountering an exception at the application’s start is both beneficial and intentional.

    The sooner the better an exception is thrown, the earlier you can fix a problem. It is getting harder when you need to search late in the development process for a missing setting, somewhere hidden in the some code.

    4. Include Settings in your local appsettings.json File

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "OpenAISettings": {
        "OpenAIKey": "1234567890987654321",
        "OpenAIEndpoint": "https://youropenaiendpoint.openai.azure.com/"
      }
    }
    

    Even though settings are not case-sensitive by default, any minor typos will result in exceptions.

    5. Additional Perk: Dependency Injection

    This pattern facilitates effortless dependency injection, allowing settings to be readily injected into other classes.

    namespace DataWebApp.Controllers
    {
        [ApiController]
        [Route("[controller]")]
        public class ChatController : ControllerBase
        {
            private readonly OpenAISettings _mySettings;
    
            public ChatController(IOptions&lt;OpenAISettings> mySettings)
            {
                _mySettings = mySettings.Value;
            }
    
            ...        
        }
    }
    

    In Conclusion

    Adopting this recommended approach not only streamlines your development process but also saves invaluable time that would otherwise be spent scouring through codebases in search of elusive settings.