Public/Add-ExpressionCacheProvider.ps1

<#
.SYNOPSIS
Registers a cache provider with ExpressionCache.
 
.DESCRIPTION
Add-ExpressionCacheProvider registers a provider specification and (if an Initialize function
is supplied) eagerly initializes it. The provider spec may be a hashtable or PSCustomObject.
It is validated and normalized by Test-ExpressionCacheProviderSpec.
 
Configuration semantics:
- No duplicate names: registration fails if a provider with the same Name already exists
  (comparison is case-insensitive).
- Eager initialization: when the spec includes an Initialize function, it is invoked once
  using parameters built from Config (New-SplatFromConfig). Required parameters are
  enforced (Assert-MandatoryParamsPresent). If Config has an 'Initialized' property,
  it is set to $true upon success.
 
Expected spec shape (examples below):
- Name or Key (string) : provider name (e.g., 'LocalFileSystemCache', 'Redis').
- Config (PSCustomObject) : provider configuration.
- Initialize (string) : optional function name to prepare backing store.
- GetOrCreate (string) : function name used to fetch/create cached values.
- ClearCache (string) : optional function name to clear cache state.
 
.PARAMETER Provider
A provider specification (hashtable/PSCustomObject). Accepts pipeline input.
May use 'Key' instead of 'Name'; it is normalized during validation.
 
.INPUTS
System.Object (provider spec via the pipeline)
 
.OUTPUTS
PSCustomObject (the normalized/registered provider spec)
 
.EXAMPLE
# Register LocalFileSystem provider with explicit functions and config
Add-ExpressionCacheProvider @{
  Key = 'LocalFileSystemCache'
  Initialize = 'Initialize-LocalFileSystem-Cache'
  GetOrCreate= 'Get-LocalFileSystem-CachedValue'
  ClearCache = 'Clear-LocalFileSystem-Cache'
  Config = @{
    Prefix = 'ExpressionCache:v1:MyApp'
    CacheFolder = "$env:TEMP/ExpressionCache"
  }
}
 
.EXAMPLE
# Register multiple providers via the pipeline
@(
  @{ Key='LocalFileSystemCache'; Config=@{ Prefix='ExpressionCache:v1:MyApp' } }
  @{ Key='Redis'; Config=@{ Prefix='ExpressionCache:v1:MyApp'; Database=2 }; Initialize='Initialize-Redis-Cache'; GetOrCreate='Get-Redis-CachedValue' }
) | Add-ExpressionCacheProvider
 
.EXAMPLE
# Preview registration/initialization without changing module state
Add-ExpressionCacheProvider @{ Key='LocalFileSystemCache'; Config=@{ Prefix='ExpressionCache:v1:MyApp' } } -WhatIf
 
.NOTES
- Duplicate names are rejected: "A provider named 'X' is already registered."
- When Initialize is present, missing mandatory parameters (as inferred from the command’s
  signature) will produce a validation error before initialization runs.
- If your provider tracks an Initialized boolean in Config, it will be set to $true after a
  successful Initialize call.
 
.LINK
Initialize-ExpressionCache
Get-ExpressionCacheProvider
Get-ExpressionCache
New-ExpressionCacheKey
about_CommonParameters
#>

function Add-ExpressionCacheProvider {
  [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
  param(
    [Parameter(Mandatory, ValueFromPipeline)]
    [object]$Provider
  )

  process {
    # 1) Validate/normalize spec (no lock needed)
    $spec = Test-ExpressionCacheProviderSpec -Spec $Provider
    $target = "Provider '$($spec.Name)'"
    $registered = $false

    # 2) Ensure the registry exists (write-lock, once)
    $needsInit = With-ReadLock { $null -eq $script:RegisteredStorageProviders }
    if ($needsInit) {
      $script:StateLock.EnterWriteLock()
      try {
        if ($null -eq $script:RegisteredStorageProviders) {
          $script:RegisteredStorageProviders = [ordered]@{}
        }
      } 
      finally { 
        $script:StateLock.ExitWriteLock() 
      }
    }

    # 3) Atomic "check-then-add" with upgradeable read → write
    $script:StateLock.EnterUpgradeableReadLock()
    try {
      if ($script:RegisteredStorageProviders.Contains($spec.Name)) {`
        throw "ExpressionCache: A provider named '$($spec.Name)' is already registered."
      }

      if ($PSCmdlet.ShouldProcess($target, 'Register')) {
        $script:StateLock.EnterWriteLock()
        try {
          # Re-check under the write lock to avoid a race
          if ($script:RegisteredStorageProviders.Contains($spec.Name)) {
            throw "ExpressionCache: A provider named '$($spec.Name)' is already registered."
          }
          $script:RegisteredStorageProviders[$spec.Name] = $spec
          $registered = $true
        } 
        finally { 
          $script:StateLock.ExitWriteLock() 
        }
      }
    }
    finally { 
      $script:StateLock.ExitUpgradeableReadLock() 
    }

    # 4) Eager initialize OUTSIDE the registry lock
    if ($registered -and $spec.Initialize) {
      if ($PSCmdlet.ShouldProcess($target, 'Initialize')) {
        try {
          $paramSet = New-SplatFromConfig -CommandName $spec.Initialize -Config $spec.Config
          Assert-MandatoryParamsPresent -CommandName $spec.Initialize -Splat $paramSet
          & $spec.Initialize @paramSet

          Set-ProviderStateValue -Provider $Provider -Key 'Initialized' -Value $true
        } 
        catch {
          # Optional: record failure for diagnostics (don’t hold the module lock here)
          Set-ProviderStateValue -Provider $Provider -Key 'InitializationError' -Value $_.Exception.Message
          throw
        }
      }
    }

    return $spec
  }
}