Complete guide to configuring the Package Script Writer application.
- Application Settings
- Dependency Injection
- Middleware Configuration
- Caching Configuration
- Environment-Specific Settings
- Logging Configuration
File: appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"PSW": {
"CacheDurationMinutes": 60,
"UmbracoVersions": [
{
"Version": "14",
"ReleaseDate": "2024-05-30",
"EndOfLifeDate": "2027-12-30",
"EndOfSecurityDate": "2027-12-30",
"IsLTS": true
},
{
"Version": "13",
"ReleaseDate": "2023-12-14",
"EndOfLifeDate": "2024-12-14",
"IsLTS": false
},
{
"Version": "12",
"ReleaseDate": "2023-06-29",
"EndOfLifeDate": "2024-06-29",
"IsLTS": false
},
{
"Version": "11",
"ReleaseDate": "2022-12-08",
"EndOfLifeDate": "2023-12-08",
"IsLTS": false
},
{
"Version": "10",
"ReleaseDate": "2022-06-16",
"EndOfLifeDate": "2025-12-16",
"EndOfSecurityDate": "2025-12-16",
"IsLTS": true
},
{
"Version": "9",
"ReleaseDate": "2021-09-28",
"EndOfLifeDate": "2023-09-28",
"IsLTS": false
},
{
"Version": "8",
"ReleaseDate": "2019-02-26",
"EndOfLifeDate": "2023-02-26",
"IsLTS": false
},
{
"Version": "7",
"ReleaseDate": "2013-11-19",
"EndOfLifeDate": "2023-09-30",
"IsLTS": false
}
]
}
}| Property | Type | Default | Description |
|---|---|---|---|
CacheDurationMinutes |
int | 60 | Cache expiration time in minutes |
UmbracoVersions |
Array | See above | Umbraco version lifecycle data |
Controls how long data is cached in memory:
- Package List: 60 minutes
- Package Versions: 60 minutes
- Template Versions: 60 minutes
Tuning Recommendations:
- Development: 5-10 minutes (faster cache refresh)
- Production: 60-120 minutes (reduce API calls)
- High Traffic: 120+ minutes (minimize external API load)
Each version object contains:
{
"Version": "14",
"ReleaseDate": "2024-05-30",
"EndOfLifeDate": "2027-12-30",
"EndOfSecurityDate": "2027-12-30",
"IsLTS": true
}| Field | Type | Required | Description |
|---|---|---|---|
Version |
string | Yes | Major version number |
ReleaseDate |
string (ISO 8601) | Yes | Official release date |
EndOfLifeDate |
string (ISO 8601) | No | End of life date |
EndOfSecurityDate |
string (ISO 8601) | No | End of security updates |
IsLTS |
boolean | Yes | Long-term support flag |
Version Status Calculation:
- Active LTS:
IsLTS = trueand current date <EndOfLifeDate - Active STS:
IsLTS = falseand current date <EndOfLifeDate - Security Only: Current date between
EndOfLifeDateandEndOfSecurityDate - EOL: Current date >
EndOfLifeDate(orEndOfSecurityDateif present)
File: Program.cs
var builder = WebApplication.CreateBuilder(args);
// MVC Services
builder.Services.AddControllersWithViews()
.AddRazorOptions(options => options.ViewLocationFormats.Add("/{0}.cshtml"));
// Infrastructure Services
builder.Services.AddHttpClient();
builder.Services.AddMemoryCache();
// Application Services (Scoped)
builder.Services.AddScoped<IScriptGeneratorService, ScriptGeneratorService>();
builder.Services.AddScoped<IPackageService, MarketplacePackageService>();
builder.Services.AddScoped<IQueryStringService, QueryStringService>();
builder.Services.AddScoped<IUmbracoVersionService, UmbracoVersionService>();
// Configuration Binding
builder.Services.Configure<PSWConfig>(
builder.Configuration.GetSection(PSWConfig.SectionName)
);
var app = builder.Build();| Service | Lifetime | Reason |
|---|---|---|
| Controllers | Scoped | Per-request |
| ScriptGeneratorService | Scoped | No state, per-request |
| MarketplacePackageService | Scoped | Uses HttpClient per request |
| QueryStringService | Scoped | No state, per-request |
| UmbracoVersionService | Scoped | No state, per-request |
| HttpClient | Scoped | Managed by HttpClientFactory |
| IMemoryCache | Singleton | Shared across requests |
builder.Services.AddHttpClient();Benefits:
- Proper disposal of HttpClient instances
- Connection pooling
- DNS refresh handling
- Named/typed clients support
Usage in Services:
public class MarketplacePackageService : IPackageService
{
private readonly IHttpClientFactory _httpClientFactory;
public MarketplacePackageService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task<PagedPackages> GetAllPackages()
{
var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync("https://marketplace.umbraco.com/...");
// ...
}
}File: Program.cs
var app = builder.Build();
// Environment-specific middleware
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts(); // HTTP Strict Transport Security
}
// HTTPS Redirection
app.UseHttpsRedirection();
// Custom Security Headers
app.UseMiddleware<SecurityHeadersMiddleware>();
// Static Files
app.UseStaticFiles();
// Routing
app.UseRouting();
// Authorization (no authentication configured)
app.UseAuthorization();
// MVC Routing
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();Order matters! The pipeline executes in the order defined:
- Exception Handler (Production only)
- HSTS (Production only)
- HTTPS Redirection (Always)
- Security Headers (Custom middleware)
- Static Files (CSS, JS, images)
- Routing (Match endpoints)
- Authorization (Check permissions)
- Endpoint Execution (Controllers)
HTTP Strict Transport Security forces HTTPS connections.
Default Configuration:
app.UseHsts();Custom Configuration (optional):
builder.Services.AddHsts(options =>
{
options.MaxAge = TimeSpan.FromDays(365);
options.IncludeSubDomains = true;
options.Preload = true;
});Headers Added:
Strict-Transport-Security: max-age=31536000; includeSubDomains
app.UseStaticFiles();Default Settings:
- Directory:
wwwroot/ - Caching: Browser caching enabled
- File Types: All common web file types
Custom Configuration (optional):
app.UseStaticFiles(new StaticFileOptions
{
OnPrepareResponse = ctx =>
{
// Set cache headers for static assets
ctx.Context.Response.Headers.Append(
"Cache-Control", "public,max-age=31536000");
}
});Registration:
builder.Services.AddMemoryCache();Configuration Options:
builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = 1024; // Maximum cache size (entries)
options.CompactionPercentage = 0.2; // Compact 20% when limit reached
options.ExpirationScanFrequency = TimeSpan.FromMinutes(5); // Scan for expired entries
});Usage in Services:
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(cacheDurationMinutes))
.SetPriority(CacheItemPriority.Normal);
memoryCache.Set(cacheKey, data, cacheEntryOptions);Cache Priorities:
CacheItemPriority.Low- Evicted firstCacheItemPriority.Normal- DefaultCacheItemPriority.High- Evicted lastCacheItemPriority.NeverRemove- Never evicted
| Key Pattern | Data | TTL |
|---|---|---|
all-packages |
Umbraco Marketplace packages | 60 min |
package-versions-{packageId} |
NuGet package versions | 60 min |
umbraco-templates |
Umbraco template versions | 60 min |
File: appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information",
"Microsoft.AspNetCore": "Debug"
}
},
"DetailedErrors": true,
"PSW": {
"CacheDurationMinutes": 5
}
}Key Differences:
- More verbose logging (
Debuglevel) - Detailed error pages enabled
- Shorter cache duration for faster iteration
File: appsettings.Production.json (not committed to repo)
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"PSW": {
"CacheDurationMinutes": 120
}
}Key Differences:
- Minimal logging (
Warninglevel only) - Longer cache duration
- No detailed errors exposed
if (app.Environment.IsDevelopment())
{
// Development-only middleware
}
else if (app.Environment.IsProduction())
{
// Production-only middleware
}
else if (app.Environment.IsStaging())
{
// Staging-specific configuration
}Environment Variable:
export ASPNETCORE_ENVIRONMENT=Development
export ASPNETCORE_ENVIRONMENT=Production
export ASPNETCORE_ENVIRONMENT=StagingProviders:
- Console (enabled in Development)
- Debug (enabled in Development)
- EventSource
| Level | Description | Example |
|---|---|---|
Trace |
Most detailed | Method entry/exit |
Debug |
Debugging info | Variable values |
Information |
Informational | Request processing |
Warning |
Warnings | Deprecated API usage |
Error |
Errors | Unhandled exceptions |
Critical |
Critical failures | Database connection lost |
public class ScriptGeneratorService : IScriptGeneratorService
{
private readonly ILogger<ScriptGeneratorService> _logger;
public ScriptGeneratorService(ILogger<ScriptGeneratorService> logger)
{
_logger = logger;
}
public string GenerateScript(PackagesViewModel model)
{
_logger.LogInformation("Generating script for project: {ProjectName}", model.ProjectName);
try
{
// ... generation logic
_logger.LogDebug("Script generated successfully");
return script;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating script");
throw;
}
}
}Use structured logging with named parameters:
// Good
_logger.LogInformation("User {UserId} generated script for {ProjectName}", userId, projectName);
// Avoid
_logger.LogInformation($"User {userId} generated script for {projectName}");Benefits:
- Better searchability in log aggregation tools
- Easier querying
- More context
builder.Services.AddControllersWithViews()
.AddRazorOptions(options =>
{
// Add custom view location formats
options.ViewLocationFormats.Add("/{0}.cshtml");
});Default View Locations:
/Views/{ControllerName}/{ViewName}.cshtml/Views/Shared/{ViewName}.cshtml
With Custom Format:
3. /{ViewName}.cshtml (root-level views)
builder.Services.AddControllersWithViews()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.WriteIndented = true;
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");Pattern Explanation:
{controller=Home}- Controller name (default: Home){action=Index}- Action name (default: Index){id?}- Optional ID parameter
Examples:
/→HomeController.Index()/Home/Index→HomeController.Index()/Home/Error/123→HomeController.Error(123)
Controllers use attribute routing:
[Route("api/[controller]")]
[ApiController]
public class ScriptGeneratorApiController : ControllerBase
{
[HttpPost("generatescript")]
public IActionResult GenerateScript([FromBody] GeneratorApiRequest request)
{
// ...
}
}Generated Routes:
POST /api/scriptgeneratorapi/generatescriptPOST /api/scriptgeneratorapi/getpackageversionsGET /api/scriptgeneratorapi/clearcache
// Register
builder.Services.Configure<PSWConfig>(
builder.Configuration.GetSection(PSWConfig.SectionName)
);
// Inject
public class MyService
{
private readonly PSWConfig _config;
public MyService(IOptions<PSWConfig> config)
{
_config = config.Value;
}
}- Use User Secrets for development
- Use Environment Variables for production
- Use Azure Key Vault for sensitive production secrets
Use separate appsettings.{Environment}.json files for each environment.
var config = builder.Configuration.GetSection("PSW").Get<PSWConfig>();
if (config.CacheDurationMinutes < 1)
{
throw new InvalidOperationException("Cache duration must be at least 1 minute");
}