A production-ready .NET class library for fetching and parsing SAML, WS-Federation (WSFED), and OpenID Connect (OIDC) metadata from multiple identity provider endpoints. Includes an IIS module for automatic metadata polling and caching.
The fastest way to try it is the console utility:
# Build (Windows)
msbuild /t:restore
msbuild /t:build /p:GenerateFullPaths=true /consoleloggerparameters:NoSummary
# Run: fetch and summarize WS-Federation metadata
IdentityMetadataFetcher.Console.exe https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml
# Run: fetch and summarize OIDC metadata
IdentityMetadataFetcher.Console.exe https://accounts.google.com/.well-known/openid-configuration
# Print raw metadata too
IdentityMetadataFetcher.Console.exe https://accounts.google.com/.well-known/openid-configuration --rawSee full details in the Console Utility section below.
- 📦 Dual Components: Core library for direct usage + IIS HTTP module for ASP.NET applications
- 🔄 Multiple Metadata Types: Support for WSFED, SAML, and OpenID Connect (OIDC) metadata formats
- 🚀 Sync & Async APIs: Choose between blocking and async/await patterns for optimal performance
- ⚙️ Highly Configurable: Control timeouts, retries, SSL validation, and error handling
- 🛡️ Production-Ready: Comprehensive error handling, thread-safe design, and full unit test coverage
- ♻️ IIS Auto-Polling: Automatic background metadata refresh with configurable intervals
- 💾 In-Memory Caching: Fast access to cached metadata in ASP.NET applications
- 🔒 Auto-Apply to IdentityModel: Optional runtime updates to System.IdentityModel configuration
- .NET Framework 4.6.2, 4.7, or 4.8
- Microsoft.IdentityModel.Protocols.OpenIdConnect 8.15.0+ (NuGet package)
- Microsoft.IdentityModel.Protocols.WsFederation 8.15.0+ (NuGet package)
- Microsoft.IdentityModel.Tokens.Saml 8.15.0+ (NuGet package)
- System.IdentityModel.Services (built-in to .NET Framework - IIS module only)
⚠️ Windows Only: This library targets .NET Framework and requires a Windows environment to build and run.📦 Microsoft.IdentityModel Migration: The library has been fully migrated to Microsoft.IdentityModel packages. Metadata is now returned as
WsFederationConfigurationorOpenIdConnectConfigurationinstead of the legacyEntityDescriptor. See MIGRATION_COMPLETE.md for full details.
- Clone the repository
- Build the
IdentityMetadataFetcher.csprojproject - Reference the resulting DLL in your application
- Add a reference to
System.IdentityModel.Metadatain your project
# Using dotnet CLI (requires Windows)
dotnet build src/IdentityMetadataFetcher/IdentityMetadataFetcher.csproj
# Or using MSBuild
msbuild src/IdentityMetadataFetcher/IdentityMetadataFetcher.csprojAdd a reference to IdentityMetadataFetcher.dll in your project and include the required using statements:
using IdentityMetadataFetcher.Models;
using IdentityMetadataFetcher.Services;using IdentityMetadataFetcher.Models;
using IdentityMetadataFetcher.Services;
// Create a fetcher instance
var fetcher = new MetadataFetcher();
// Define an issuer endpoint
var endpoint = new IssuerEndpoint
{
Id = "azure-ad",
Endpoint = "https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml",
Name = "Azure AD"
};
// Fetch metadata synchronously
var result = fetcher.FetchMetadata(endpoint);
if (result.IsSuccess)
{
// Use the metadata
var metadata = result.Metadata;
Console.WriteLine($"✓ Successfully fetched metadata from {result.Endpoint.Name}");
Console.WriteLine($" Fetched at: {result.FetchedAt:O}");
}
else
{
Console.WriteLine($"✗ Error: {result.ErrorMessage}");
}// Async fetch from single endpoint
var result = await fetcher.FetchMetadataAsync(endpoint);
if (result.IsSuccess)
{
Console.WriteLine($"✓ Metadata retrieved successfully");
}var endpoints = new[]
{
new IssuerEndpoint
{
Id = "saml-provider",
Endpoint = "https://issuer1.example.com/metadata",
Name = "SAML Identity Provider"
},
new IssuerEndpoint
{
Id = "wsfed-provider",
Endpoint = "https://issuer2.example.com/metadata",
Name = "WS-Fed Identity Provider"
}
};
// Fetch from all endpoints
var results = fetcher.FetchMetadataFromMultipleEndpoints(endpoints);
foreach (var result in results)
{
if (result.IsSuccess)
{
Console.WriteLine($"✓ {result.Endpoint.Name}");
}
else
{
Console.WriteLine($"✗ {result.Endpoint.Name}: {result.ErrorMessage}");
}
}// Fetch from all endpoints asynchronously for better performance
var results = await fetcher.FetchMetadataFromMultipleEndpointsAsync(endpoints);
// Process results
var successCount = results.Count(r => r.IsSuccess);
var failureCount = results.Count(r => !r.IsSuccess);
Console.WriteLine($"Completed: {successCount} succeeded, {failureCount} failed");var endpoint = new IssuerEndpoint
{
Id = "azure-ad",
Endpoint = "https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml",
Name = "Azure AD"
};
var result = await fetcher.FetchMetadataAsync(endpoint);
if (result.IsSuccess && result.Metadata != null)
{
// Access WsFederationConfiguration
var config = result.Metadata.Configuration;
Console.WriteLine($"Issuer: {config.Issuer}");
Console.WriteLine($"Token Endpoint: {config.TokenEndpoint}");
// Access signing keys
foreach (var key in config.SigningKeys)
{
if (key is Microsoft.IdentityModel.Tokens.X509SecurityKey x509Key)
{
Console.WriteLine($"Certificate: {x509Key.Certificate.Subject}");
}
}
// Access raw XML metadata if needed
var rawXml = result.RawMetadata;
}var endpoint = new IssuerEndpoint
{
Id = "google-oidc",
Endpoint = "https://accounts.google.com/.well-known/openid-configuration",
Name = "Google OIDC"
};
var result = await fetcher.FetchMetadataAsync(endpoint);
if (result.IsSuccess && result.Metadata is OpenIdConnectMetadataDocument oidcDoc)
{
// Access OpenIdConnectConfiguration
var config = oidcDoc.Configuration;
Console.WriteLine($"Issuer: {config.Issuer}");
Console.WriteLine($"Authorization Endpoint: {config.AuthorizationEndpoint}");
Console.WriteLine($"Token Endpoint: {config.TokenEndpoint}");
Console.WriteLine($"UserInfo Endpoint: {config.UserInfoEndpoint}");
// Access signing keys
foreach (var key in config.SigningKeys)
{
Console.WriteLine($"Key ID: {key.KeyId}");
}
// Access raw JSON metadata if needed
var rawJson = result.RawMetadata;
}var result = await fetcher.FetchMetadataAsync(endpoint);
if (result.IsSuccess)
{
if (result.Metadata is WsFederationMetadataDocument wsFedDoc)
{
// Process WS-Federation/SAML metadata
Console.WriteLine($"WS-Fed/SAML Issuer: {wsFedDoc.Issuer}");
}
else if (result.Metadata is OpenIdConnectMetadataDocument oidcDoc)
{
// Process OIDC metadata
Console.WriteLine($"OIDC Issuer: {oidcDoc.Issuer}");
}
}Control the behavior of metadata fetching using MetadataFetchOptions:
var options = new MetadataFetchOptions
{
DefaultTimeoutMs = 30000, // 30 second timeout (default)
ContinueOnError = true, // Keep fetching even if one endpoint fails
ValidateServerCertificate = true, // Validate SSL/TLS certificates (recommended)
MaxRetries = 2, // Retry failed requests up to 2 times
CacheMetadata = false, // Disable caching (reserved for future use)
CacheDurationMinutes = 60 // Cache duration if enabled (future)
};
var fetcher = new MetadataFetcher(options);| Option | Type | Default | Description |
|---|---|---|---|
DefaultTimeoutMs |
int | 30000 | HTTP request timeout in milliseconds |
ContinueOnError |
bool | true | Continue fetching remaining endpoints if one fails |
ValidateServerCertificate |
bool | true | Validate SSL/TLS certificates (disable only for dev/test) |
MaxRetries |
int | 0 | Number of retry attempts on failure (0-5) |
CacheMetadata |
bool | false | Reserved for future caching implementation |
CacheDurationMinutes |
int | 60 | Cache TTL in minutes (reserved for future) |
Development with Self-Signed Certificates:
var options = new MetadataFetchOptions
{
ValidateServerCertificate = false // ⚠️ WARNING: Only for development!
};
var fetcher = new MetadataFetcher(options);Resilient Fetching with Retries:
var options = new MetadataFetchOptions
{
MaxRetries = 3, // Retry up to 3 times
DefaultTimeoutMs = 15000, // 15 second timeout
ContinueOnError = true // Don't stop on first failure
};
var fetcher = new MetadataFetcher(options);Per-Endpoint Timeout Override:
var endpoint = new IssuerEndpoint
{
Id = "slow-issuer",
Endpoint = "https://slow-issuer.example.com/metadata",
Name = "Slow Issuer",
Timeout = 60000 // 60 second timeout just for this endpoint
};
var result = fetcher.FetchMetadata(endpoint);The IdentityMetadataFetcher.Iis module is an ASP.NET HTTP Module that automatically polls SAML/WSFED metadata endpoints and maintains an in-memory cache. This enables ASP.NET applications to use up-to-date metadata for identity validation without manual intervention.
Copy both DLLs to your ASP.NET application's bin directory:
IdentityMetadataFetcher.dll(core library)IdentityMetadataFetcher.Iis.dll(IIS module)
Add the module registration to your Web.config in the <system.webServer> section:
<configuration>
<system.webServer>
<modules>
<add name="SamlMetadataPollingModule"
type="IdentityMetadataFetcher.Iis.Modules.MetadataPollingHttpModule, IdentityMetadataFetcher.Iis" />
</modules>
</system.webServer>
</configuration>Add configuration sections to define metadata endpoints and polling behavior:
<configuration>
<configSections>
<section name="samlMetadataPolling"
type="IdentityMetadataFetcher.Iis.Configuration.MetadataPollingConfigurationSection, IdentityMetadataFetcher.Iis" />
</configSections>
<samlMetadataPolling enabled="true"
autoApplyIdentityModel="false"
pollingIntervalMinutes="60"
httpTimeoutSeconds="30"
validateServerCertificate="true"
maxRetries="1">
<issuers>
<!-- Azure AD -->
<add id="azure-ad"
endpoint="https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml"
name="Azure Active Directory" />
<!-- Auth0 with custom timeout -->
<add id="auth0"
endpoint="https://example.auth0.com/samlp/metadata"
name="Auth0"
timeoutSeconds="45" />
<!-- Okta -->
<add id="okta"
endpoint="https://dev-12345.okta.com/app/123/sso/saml/metadata"
name="Okta" />
</issuers>
</samlMetadataPolling>
</configuration>Root Element: <samlMetadataPolling>
| Attribute | Type | Default | Required | Description |
|---|---|---|---|---|
enabled |
bool | true | No | Enable/disable the polling service |
autoApplyIdentityModel |
bool | false | No | Automatically update System.IdentityModel with fetched metadata |
pollingIntervalMinutes |
int | 60 | No | How often to poll (1-10080 minutes) |
httpTimeoutSeconds |
int | 30 | No | HTTP request timeout (5-300 seconds) |
validateServerCertificate |
bool | true | No | Validate SSL/TLS certificates |
maxRetries |
int | 1 | No | Retry failed requests (0-5) |
Child Element: <issuers> Collection
Each <add> element defines an issuer endpoint:
| Attribute | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier for the issuer |
endpoint |
string | Yes | Full URL to the metadata endpoint |
name |
string | Yes | Human-readable issuer name |
timeoutSeconds |
int | No | Override default timeout for this endpoint (5-300) |
The IIS module can optionally apply fetched metadata directly to System.IdentityModel configuration at runtime. This feature automatically updates your application's identity configuration with the latest certificates and endpoints from your identity providers.
Set autoApplyIdentityModel="true" in your configuration:
<samlMetadataPolling enabled="true"
autoApplyIdentityModel="true"
pollingIntervalMinutes="60"
httpTimeoutSeconds="30"
authFailureRecoveryIntervalMinutes="5">
<issuers>
<add id="azure-ad"
endpoint="https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml"
name="Azure Active Directory" />
</issuers>
</samlMetadataPolling>When autoApplyIdentityModel is enabled, the module automatically:
- Updates Signing Certificates: Extracts X.509 certificates from metadata and applies them to the IdentityModel configuration
- Updates Issuer Information: Configures valid issuers based on EntityID from metadata
- Updates Endpoints: Applies SSO and other service endpoints from the metadata
- Maintains Security: Only applies valid, properly signed metadata
When autoApplyIdentityModel is enabled, the module also provides automatic recovery from certificate rotation failures:
- Detects Certificate Trust Failures: The module intercepts
System.IdentityModelauthentication failures and analyzes whether they're caused by untrusted issuer certificates - Identifies the Issuer: Extracts the issuer identifier from the exception to determine which identity provider's metadata needs refreshing
- Checks Polling Threshold: Verifies that sufficient time has elapsed since the last forced poll for that issuer (configurable via
authFailureRecoveryIntervalMinutes) - Refreshes Metadata: Immediately polls the issuer's metadata endpoint to check for certificate rotation
- Applies New Configuration: If new certificates are found, applies them to
System.IdentityModelconfiguration - Allows Retry: Subsequent authentication requests will use the updated certificates
<samlMetadataPolling enabled="true"
autoApplyIdentityModel="true"
pollingIntervalMinutes="60"
httpTimeoutSeconds="30"
authFailureRecoveryIntervalMinutes="5">
<!-- authFailureRecoveryIntervalMinutes: minimum time (1-60 minutes) between
forced metadata refreshes triggered by authentication failures.
Default: 5 minutes -->
<issuers>
<add id="azure-ad"
endpoint="https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml"
name="Azure Active Directory" />
</issuers>
</samlMetadataPolling>- Current Request Fails: The request that triggered the recovery will still fail with an authentication error
- Subsequent Requests Succeed: After metadata is refreshed, subsequent authentication requests will succeed with the new certificates
- Rate Limiting: The
authFailureRecoveryIntervalMinutessetting prevents excessive polling during rapid authentication failures - Asynchronous Recovery: Recovery happens in the background and doesn't block the failing request
- Synchronous Recovery: Blocks the request thread while attempting recovery, then redirects on success
- Diagnostic Logging: All recovery attempts are logged to
System.Diagnostics.Tracefor monitoring
- Identity provider (e.g., Azure AD) rotates their signing certificates
- User attempts to authenticate before scheduled polling occurs
- Authentication fails with certificate trust error (e.g., ID4037)
- Module detects the failure, identifies Azure AD as the issuer
- Module immediately fetches fresh metadata from Azure AD
- New certificates are applied to IdentityModel configuration
- User's next authentication attempt succeeds with new certificates
Subscribe to trace events to monitor recovery operations:
// In your application startup or Global.asax
System.Diagnostics.Trace.Listeners.Add(
new System.Diagnostics.TextWriterTraceListener("app_trace.log"));
// Recovery events will be logged:
// - "Authentication error detected"
// - "Detected certificate trust failure"
// - "Attempting metadata refresh for issuer"
// - "Successfully recovered from authentication failure"
⚠️ Important: TheautoApplyIdentityModelfeature is disabled by default for security reasons.
Before enabling this feature in production:
- Verify Metadata Sources: Ensure all configured endpoints are from trusted identity providers
- Use HTTPS: Only fetch metadata from HTTPS endpoints
- Certificate Validation: Keep
validateServerCertificate="true"(the default) to prevent man-in-the-middle attacks - Monitor Changes: Subscribe to the
MetadataUpdatedevent to log configuration changes - Test Thoroughly: Test in a staging environment first to ensure proper behavior
- Rate Limiting: Configure
authFailureRecoveryIntervalMinutesappropriately to prevent DoS from excessive polling
<samlMetadataPolling enabled="true"
autoApplyIdentityModel="true"
pollingIntervalMinutes="120"
httpTimeoutSeconds="30"
validateServerCertificate="true"
maxRetries="2"
authFailureRecoveryIntervalMinutes="5">
<issuers>
<!-- Only trusted, HTTPS endpoints -->
<add id="azure-ad"
endpoint="https://login.microsoftonline.com/your-tenant-id/federationmetadata/2007-06/federationmetadata.xml"
name="Azure Active Directory" />
</issuers>
</samlMetadataPolling>Use event handlers to monitor when IdentityModel configuration is updated:
var service = MetadataPollingHttpModule.PollingService;
service.MetadataUpdated += (sender, e) =>
{
if (e.AutoApplied)
{
logger.LogInformation($"IdentityModel configuration updated for {e.IssuerName} at {e.UpdatedAt:O}");
// Optionally trigger cache invalidation or other actions
InvalidateAuthenticationCache();
}
};If you prefer manual control over IdentityModel configuration, keep the default setting or explicitly disable:
<samlMetadataPolling enabled="true"
autoApplyIdentityModel="false"
pollingIntervalMinutes="60">
<!-- Metadata will be fetched and cached but NOT applied to IdentityModel -->
<!-- Authentication failure recovery will NOT be active -->
<issuers>
<add id="azure-ad"
endpoint="https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml"
name="Azure Active Directory" />
</issuers>
</samlMetadataPolling>With auto-apply disabled, you can still:
- Access cached metadata via
MetadataPollingHttpModule.MetadataCache - Manually apply configuration changes when needed
- Implement custom validation logic before applying updates
A Windows-only console tool is included to fetch metadata from a URL and display a friendly summary. Supports both WS-Federation/SAML and OIDC metadata formats.
- Project:
src/IdentityMetadataFetcher.Console - Target:
.NET Framework 4.8 - Usage:
IdentityMetadataFetcher.Console <metadata-url> [--raw]
# Build (Windows)
msbuild /t:restore
msbuild /t:build /p:GenerateFullPaths=true /consoleloggerparameters:NoSummary
# Run: Azure AD WS-Federation metadata
IdentityMetadataFetcher.Console.exe https://login.microsoftonline.com/common/federationmetadata/2007-06/federationmetadata.xml
# Run: Google OIDC metadata
IdentityMetadataFetcher.Console.exe https://accounts.google.com/.well-known/openid-configuration
# Include raw metadata output (XML or JSON)
IdentityMetadataFetcher.Console.exe https://accounts.google.com/.well-known/openid-configuration --rawThe console utility automatically detects the metadata format and displays:
WS-Federation/SAML Metadata:
- Issuer
- Endpoints (Token Endpoint, Passive STS, etc.)
- Signing Certificates
- Signing Keys
OIDC Metadata:
- Issuer
- Endpoints (Authorization, Token, UserInfo, JWKs URI, etc.)
- Signing Certificates
- Signing Keys
Before building the project, ensure you have the following installed:
- Windows OS (required for .NET Framework)
- .NET Framework 4.5 or higher - Download
- Visual Studio 2015+ (recommended) or MSBuild 14.0+
- NUnit 3.x (for running tests, optional)
-
Open the solution file:
start IdentityMetadataFetcher.sln
-
In Visual Studio:
- Select Build > Build Solution (or press
Ctrl+Shift+B) - Output appears in
bin/Debugorbin/Release
- Select Build > Build Solution (or press
# Navigate to repository root
cd /path/to/IdentityMetadataFetcher
# Restore NuGet packages (if needed)
nuget restore IdentityMetadataFetcher.sln
# Debug build
msbuild IdentityMetadataFetcher.sln /p:Configuration=Debug
# Release build
msbuild IdentityMetadataFetcher.sln /p:Configuration=Release
# Clean build
msbuild IdentityMetadataFetcher.sln /t:Clean /p:Configuration=Debug
msbuild IdentityMetadataFetcher.sln /p:Configuration=Debug# Build entire solution
dotnet build IdentityMetadataFetcher.sln
# Build specific project
dotnet build src/IdentityMetadataFetcher/IdentityMetadataFetcher.csproj
# Build for Release
dotnet build IdentityMetadataFetcher.sln --configuration Release- Open Test Explorer:
Test > Windows > Test Explorer - Tests appear in the list
- Click Run All or select individual tests
- View results in the Test Explorer window
# Install NUnit console runner (first time only)
nuget install NUnit.ConsoleRunner -Version 3.16.3 -OutputDirectory packages
# Run all tests
./packages/NUnit.ConsoleRunner.3.16.3/tools/nunit3-console.exe \
tests/IdentityMetadataFetcher.Tests/bin/Debug/IdentityMetadataFetcher.Tests.dll
# Run tests with XML output
./packages/NUnit.ConsoleRunner.3.16.3/tools/nunit3-console.exe \
tests/IdentityMetadataFetcher.Tests/bin/Debug/IdentityMetadataFetcher.Tests.dll \
--result=TestResults.xml# Run all tests in solution
dotnet test IdentityMetadataFetcher.sln
# Run with detailed output
dotnet test IdentityMetadataFetcher.sln --verbosity detailed
# Run specific test project
dotnet test tests/IdentityMetadataFetcher.Tests/IdentityMetadataFetcher.Tests.csprojAfter building, find the compiled assemblies:
src/IdentityMetadataFetcher/bin/Debug/IdentityMetadataFetcher.dll
src/IdentityMetadataFetcher/bin/Release/IdentityMetadataFetcher.dll
src/IdentityMetadataFetcher.Iis/bin/Debug/IdentityMetadataFetcher.Iis.dll
src/IdentityMetadataFetcher.Iis/bin/Release/IdentityMetadataFetcher.Iis.dll
Verify the build was successful:
# Check if DLLs were created
ls src/IdentityMetadataFetcher/bin/Debug/IdentityMetadataFetcher.dll
ls src/IdentityMetadataFetcher.Iis/bin/Debug/IdentityMetadataFetcher.Iis.dll
# Run quick test
dotnet test tests/IdentityMetadataFetcher.Tests/IdentityMetadataFetcher.Tests.csproj- IIS_MODULE_USAGE.md - Detailed IIS module documentation
- BUILD.md - Advanced build instructions and CI/CD examples
- QUICKREF.md - Quick reference guide with code snippets
- DESIGN.md - Architecture and design documentation
- CHANGELOG.md - Version history and changes
MetadataFetcher- Main service for fetching metadataIMetadataFetcher- Service interface for dependency injectionIssuerEndpoint- Endpoint configuration modelMetadataFetchResult- Result container with success/failure informationMetadataFetchOptions- Configuration options for fetcher behavior
MetadataPollingHttpModule- HTTP module for ASP.NETMetadataCache- Thread-safe metadata cacheMetadataPollingService- Background polling service
-
SSL/TLS Certificate Validation: By default, the library validates server certificates. Only disable validation (
ValidateServerCertificate = false) in development/test environments. -
Metadata Sources: Only fetch metadata from trusted issuer endpoints.
-
Auto-Apply Feature: The
autoApplyIdentityModelsetting is disabled by default. Only enable it for trusted metadata sources over HTTPS. -
Error Information: Exception details may contain sensitive information about endpoints. Handle exceptions carefully in production environments.
-
Timeout Configuration: Set appropriate timeouts to prevent resource exhaustion from slow or unresponsive endpoints.
- Use Async Methods: For better scalability when fetching from multiple endpoints
- Batch Operations: Fetch from multiple endpoints in a single operation rather than individual calls
- IIS Module: Use the IIS module for ASP.NET applications to avoid repeated fetching
- Appropriate Timeouts: Set realistic timeout values based on network conditions
| Problem | Solution |
|---|---|
| HttpRequestException | Verify endpoint URL, check network/firewall, validate SSL certificate, increase timeout |
| MetadataFetchException | Ensure endpoint returns valid metadata |
| TimeoutException | Increase DefaultTimeoutMs or per-endpoint Timeout, check endpoint responsiveness |
| Build Errors | Ensure .NET Framework 4.5+ is installed, restore NuGet packages, clean and rebuild |
| IIS Module Not Loading | Verify DLLs are in bin directory, check Web.config registration, review IIS logs |
This library is provided as-is for use in your applications.
For issues, questions, or contributions, please refer to:
- Source code documentation and inline comments
- Unit tests for usage examples
- Additional documentation files in the repository
Version: 1.0.0
Target Framework: .NET Framework 4.5+
Status: Production Ready ✅