Skip to content

Commit

Permalink
Add copy button to code elements.
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott Galloway committed Sep 27, 2024
1 parent 83cef92 commit 21a863d
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 45 deletions.
1 change: 0 additions & 1 deletion Mostlylucid.Shared/Config/NewsletterConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,5 @@ public class NewsletterConfig : IConfigSection
public static string Section => "Newsletter";

public string SchedulerServiceUrl { get; set; } = string.Empty;

public string AppHostUrl { get; set; } = string.Empty;
}
158 changes: 158 additions & 0 deletions Mostlylucid/Markdown/customconfigsectionextensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Custom Config Section Extensions
<!--category-- C#, ASP.NET -->
<datetime class="hidden">2024-09-27T06:20</datetime>
# Introduction
It seems like everyone has a version of this code, I first came across this approach from [Filip W](https://www.strathweb.com/2016/09/strongly-typed-configuration-in-asp-net-core-without-ioptionst/) and recently my old colleague Phil Haack has [his version](https://haacked.com/archive/2024/07/18/better-config-sections/).

Just for completion this is how I do it.

[TOC]

# Why?
The why is to make it easier to work with configuration. At the moment the premier way is using `IOptions<T>` and `IOptionsSnapshot<T>` but this is a bit of a pain to work with. This approach allows you to work with the configuration in a more natural way.
The limitations are that you can't use the `IOptions<T>` pattern, so you don't get the ability to reload the configuration without restarting the application. However honestly this is something I almost never want to do.

# The Code
In my version I use the recent static Interface members to specify that all instances of this class must declare a `Section` property. This is used to get the section from the configuration.

```csharp
public interface IConfigSection {
public static abstract string Section { get; }
}
```
So for each implementation you then specify what section this should be bound to:

```csharp
public class NewsletterConfig : IConfigSection
{
public static string Section => "Newsletter";

public string SchedulerServiceUrl { get; set; } = string.Empty;
public string AppHostUrl { get; set; } = string.Empty;
}
```

In this case it's looking for a section in the configuration called `Newsletter`.

```json
"Newsletter": {
"SchedulerServiceUrl" : "http://localhost:5000",
"AppHostUrl" : "https://localhost:7240"

}

```

We will then be able to bind this in the `Program.cs` file like so:

```csharp

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;
services.ConfigurePOCO<NewsletterConfig>(config);

```

We can also get the value of the config in the `Program.cs` file like so:

```csharp
var newsletterConfig = services.ConfigurePOCO<NewsletterConfig>(config);

```

# The Extension Method
To enable all this we have a fairly simple extension method that does the work of binding the configuration to the class.

The code below allows the following:

```csharp
// These get the values and bind them to the class while adding these to Singleton Scope
var newsletterConfig = services.ConfigurePOCO<NewsletterConfig>(config);
var newsletterConfig = services.ConfigurePOCO<NewsletterConfig>(configuration.GetSection(NewsletterConfig.Section));
// Or for Builder...These obviously only work for ASP.NET Core applications, take them out if you are using this in a different context.
var newsletterConfig = builder.Configure<NewsletterConfig>();
var newsletterConfig = builder.Configure<NewsletterConfig>(NewsletterConfig.Section);
// These just return a dictionary, which can be useful to get all the values in a section
var newsletterConfig = builder.GetConfigSection<NewsletterConfig>();

```

This is all enabled by the following extension class.

You can see that the main impetus of this is using the static interface members to specify the section name. This is then used to get the section from the configuration.


```csharp
namespace Mostlylucid.Shared.Config;

public static class ConfigExtensions {
public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, IConfigurationSection configuration)
where TConfig : class, new() {
if (services == null) throw new ArgumentNullException(nameof(services));
if (configuration == null) throw new ArgumentNullException(nameof(configuration));

var config = new TConfig();
configuration.Bind(config);
services.AddSingleton(config);
return config;
}

public static TConfig ConfigurePOCO<TConfig>(this IServiceCollection services, ConfigurationManager configuration)
where TConfig : class, IConfigSection, new()
{
var sectionName = TConfig.Section;
var section = configuration.GetSection(sectionName);
return services.ConfigurePOCO<TConfig>(section);
}

public static TConfig Configure<TConfig>(this WebApplicationBuilder builder)
where TConfig : class, IConfigSection, new() {
var services = builder.Services;
var configuration = builder.Configuration;
var sectionName = TConfig.Section;
return services.ConfigurePOCO<TConfig>(configuration.GetSection(sectionName));
}


public static TConfig GetConfig<TConfig>(this WebApplicationBuilder builder)
where TConfig : class, IConfigSection, new() {
var configuration = builder.Configuration;
var sectionName = TConfig.Section;
var section = configuration.GetSection(sectionName).Get<TConfig>();
return section;

}

public static Dictionary<string, object> GetConfigSection(this IConfiguration configuration, string sectionName) {
var section = configuration.GetSection(sectionName);
var result = new Dictionary<string, object>();
foreach (var child in section.GetChildren()) {
var key = child.Key;
var value = child.Value;
result.Add(key, value);
}

return result;
}

public static Dictionary<string, object> GetConfigSection<TConfig>(this WebApplicationBuilder builder)
where TConfig : class, IConfigSection, new() {
var configuration = builder.Configuration;
var sectionName = TConfig.Section;
return configuration.GetConfigSection(sectionName);
}
}
```

# In Use
To use these is pretty simple. In any class where you need this config you can simply inject it like so:

```csharp
public class NewsletterService(NewsletterConfig config {

}
```


# In Conclusion
Well that's it...pretty simple but it's a technique I use in all of my projects. It's a nice way to work with configuration and I think it's a bit more natural than the `IOptions<T>` pattern.
84 changes: 41 additions & 43 deletions Mostlylucid/Views/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@
x-data="globalSetup()"
x-init="themeInit()"
:class="isMobileMenuOpen ? 'max-h-screen overflow-hidden relative' : ''"
class="bg-custom-light-bg dark:bg-custom-dark-bg "
>
<header class="sticky top-0 z-50 bg-white dark:bg-custom-dark-bg shadow-md container mx-auto flex flex-wrap items-center justify-between px-4 mb-3 py-1 w-full rounded border-b border-x border-l-neutral-600 border-r-neutral-600 border-b-neutral-600 print:!hidden" id="header">
class="bg-custom-light-bg dark:bg-custom-dark-bg ">
<partial name="_Toast" />
<header class="sticky top-0 z-40 bg-white dark:bg-custom-dark-bg shadow-md container mx-auto flex flex-wrap items-center justify-between px-4 mb-3 py-1 w-full rounded border-b border-x border-l-neutral-600 border-r-neutral-600 border-b-neutral-600 print:!hidden" id="header">

<ul class="flex items-center w-full print:hidden">
<!-- First list item (logo) stays on the left -->
<li>
<a asp-action="Index" asp-controller="Home" hx-swap="show:window:top" hx-target="#contentcontainer" hx-boost="true" class="flex items-center">
<a asp-action="Index" asp-controller="Home" hx-swap="show:window:top" hx-target="#contentcontainer" hx-boost="true" class="flex items-center">
<span class="bx bx-home-alt-2 lg:text-2xl font-semibold mr-2 transition-colors group-hover:text-green dark:text-white dark:group-hover:text-secondary print:hidden"></span>

<div class="svg-container">
Expand All @@ -95,8 +95,7 @@
height="30px"
alt="mostlylucid limited"
class="hidden lg:block img-filter-dark"
:class="{ 'img-filter-dark': !isDarkMode }"
/>
:class="{ 'img-filter-dark': !isDarkMode }"/>

<!-- Small screen logo (shown on small screens and hidden on large screens) -->
<img
Expand All @@ -105,62 +104,61 @@
width="30px"
height="20px"
alt="mostlylucid limited"
class="block lg:hidden"
/>
class="block lg:hidden"/>
</div>
</a>
</li>
<li class="ml-auto"></li>
<li class="group relative mb-1 hidden lg:block ml-2" id="typeaheadelement">
<div hx-trigger="load" hx-get="/typeahead" hx-target="#typeaheadelement" hx-swap="innerHTML"></div>
<div hx-trigger="load" hx-get="/typeahead" hx-target="#typeaheadelement" hx-swap="innerHTML"></div>
</li>

<li class="group relative mb-1 hidden lg:block ml-2">
<div class="flex items-center space-x-4">
@if (Model != null)
{
if (Model.Authenticated)
{
<div class="flex items-center space-x-4">
<a data-logout-link asp-action="Logout" asp-controller="Login" target="_blank" class="btn btn-outline btn-sm text-blue-dark dark:text-white">
@if (Model.AvatarUrl != null)
{
<img src="@Model.AvatarUrl" width="24px" height="24px" class="h-6 w-6 rounded-full" alt="User Avatar" />
}
<span class="text-body text-blue-dark dark:text-white">@Model.Name</span>
<i class="bx bx-log-out mr-2 text-l lg:text-xl"></i>
</a>
</div>
}
else
{
<div class="w-[200px] h-[39px] overflow-hidden rounded">
<div id="google_button"></div>
</div>
}
if (Model.Authenticated)
{
<div class="flex items-center space-x-4">
<a data-logout-link asp-action="Logout" asp-controller="Login" target="_blank" class="btn btn-outline btn-sm text-blue-dark dark:text-white">
@if (Model.AvatarUrl != null)
{
<img src="@Model.AvatarUrl" width="24px" height="24px" class="h-6 w-6 rounded-full" alt="User Avatar"/>
}
<span class="text-body text-blue-dark dark:text-white">@Model.Name</span>
<i class="bx bx-log-out mr-2 text-l lg:text-xl"></i>
</a>
</div>
}
else
{
<div class="w-[200px] h-[39px] overflow-hidden rounded">
<div id="google_button"></div>
</div>
}
}
</div>
</li>

<li class="group relative my-1 mx-4 px-2 hidden lg:block border-neutral-400 dark:border-neutral-600 border rounded-lg">
<partial name="_Socials" />
<partial name="_Socials"/>
</li>
<li class="group relative mb-1 ml-2">
<a href="/search" hx-boost="true" hx-target="#contentcontainer" hx-swap="show:window:top" class="flex items-center text-l lg:text-xl font-medium text-primary group-hover:text-green dark:text-white space-x-2 hover:text-secondary dark:hover:text-secondary transition-colors">
<a href="/search" hx-boost="true" hx-target="#contentcontainer" hx-swap="show:window:top" class="flex items-center text-l lg:text-xl font-medium text-primary group-hover:text-green dark:text-white space-x-2 hover:text-secondary dark:hover:text-secondary transition-colors">
<i class="text-l lg:text-2xl bx bx-search"></i>
<span class="ml-2 hidden lg:block">Search</span>
</a>
</li>

<li class="group relative mb-1 ml-2">
<a href="/blog/aboutme" hx-boost="true" hx-target="#contentcontainer" hx-swap="show:window:top" class="relative z-30 block px-2 font-body text-l lg:text-xl font-medium text-primary transition-colors group-hover:text-green dark:text-white dark:group-hover:text-secondary">Intro</a>
<a href="/blog/aboutme" hx-boost="true" hx-target="#contentcontainer" hx-swap="show:window:top" class="relative z-30 block px-2 font-body text-l lg:text-xl font-medium text-primary transition-colors group-hover:text-green dark:text-white dark:group-hover:text-secondary">Intro</a>
</li>

<li class="group relative mb-1 ml-2">
<a href="/blog" hx-boost="true" hx-target="#contentcontainer" hx-swap="show:window:top" class="relative z-30 block px-2 font-body text-l lg:text-xl font-medium text-primary transition-colors group-hover:text-green dark:text-white dark:group-hover:text-secondary">Blog</a>
<a href="/blog" hx-boost="true" hx-target="#contentcontainer" hx-swap="show:window:top" class="relative z-30 block px-2 font-body text-l lg:text-xl font-medium text-primary transition-colors group-hover:text-green dark:text-white dark:group-hover:text-secondary">Blog</a>
</li>


<li class="group relative mb-1 ml-2 hidden lg:block">
<a href="/rss" target="_blank" class="flex items-center text-l lg:text-xl space-x-2 hover:text-secondary dark:hover:text-secondary transition-colors">
<i class="text-l lg:text-2xl bx bx-rss"></i>
Expand All @@ -169,7 +167,7 @@
</li>

<li class="group relative mb-0.5 ml-2">
<a asp-action="Index" hx-boost="true" hx-swap="show:window:top" hx-target="#contentcontainer" asp-controller="Contact" class="flex items-center text-l lg:text-xl space-x-2 text-primary dark:text-white hover:text-secondary dark:hover:text-secondary transition-colors" data-umami-event="Contact Click">
<a asp-action="Index" hx-boost="true" hx-swap="show:window:top" hx-target="#contentcontainer" asp-controller="Contact" class="flex items-center text-l lg:text-xl space-x-2 text-primary dark:text-white hover:text-secondary dark:hover:text-secondary transition-colors" data-umami-event="Contact Click">
<i class='bx bx-mail-send text-l lg:text-2xl'></i>
<span class="ml-0.5 hidden lg:block">Contact</span>
</a>
Expand All @@ -184,16 +182,16 @@

</header>

<div class="container mx-auto" id="contentcontainer">
@RenderBody()
</div>
<div class="container mx-auto" id="contentcontainer">
@RenderBody()
</div>
<div class="container pb-8 mx-auto">
<div
class="flex flex-col items-center justify-between sm:flex-row sm:py-2">
<div class="mr-auto flex flex-col items-center sm:flex-row">
<a asp-action="Index" asp-controller="Home" hx-swap="show:window:top" hx-target="#contentcontainer" hx-boost="true" class="mr-auto sm:mr-6">
<a asp-action="Index" asp-controller="Home" hx-swap="show:window:top" hx-target="#contentcontainer" hx-boost="true" class="mr-auto sm:mr-6">
<div class="svg-container">
<img src="/img/logo.svg" asp-append-version="true" width="180px" height="30px" alt="logo" :class="{ 'img-filter-dark': !isDarkMode }"/>
<img src="/img/logo.svg" asp-append-version="true" width="180px" height="30px" alt="logo" :class="{ 'img-filter-dark': !isDarkMode }"/>
</div>
</a>
<p class="pt-5 font-body font-light text-primary dark:text-white sm:pt-0">
Expand All @@ -202,7 +200,7 @@
</div>
<partial name="_Socials"/>
</div>

</div>
<noscript>
<style>
Expand All @@ -212,8 +210,8 @@
</style>
</noscript>
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js" async></script>
<script src="~/js/dist/main.js" asp-append-version="true" data-cfasync="false" > </script>
<script src="@(ViewBag.UmamiPath + "/" +ViewBag.UmamiScript)" data-website-id="@ViewBag.UmamiWebsiteId"></script>
<script src="~/js/dist/main.js" asp-append-version="true" data-cfasync="false"> </script>
<script src="@(ViewBag.UmamiPath + "/" + ViewBag.UmamiScript)" data-website-id="@ViewBag.UmamiWebsiteId"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
8 changes: 8 additions & 0 deletions Mostlylucid/Views/Shared/_Toast.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div id="toast" class="toast toast-bottom fixed z-50 hidden overflow-y-hidden">
<div id="toast-message" class="alert alert-success alert-warning alert-error alert-info">
<div>
<span id="toast-text">Notification message</span>
</div>
</div>
<p class="hidden right-1"></p>
</div>
1 change: 0 additions & 1 deletion Mostlylucid/src/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
@tailwind utilities;



.bg-red {
--tw-bg-opacity: 1;
background-color: rgb(128 0 0 / var(--tw-bg-opacity));
Expand Down
Loading

0 comments on commit 21a863d

Please sign in to comment.