Callbacks & Events
AvantiPoint Packages allows you to react to package lifecycle events through the INuGetFeedActionHandler interface. This is useful for:
- Sending email notifications when packages are published
- Collecting download metrics and analytics
- Restricting access to specific packages based on user licenses
- Monitoring for security concerns
- Integrating with external systems
The INuGetFeedActionHandler Interface
public interface INuGetFeedActionHandler
{
Task<bool> CanDownloadPackage(string packageId, string version);
Task OnPackageDownloaded(string packageId, string version);
Task OnSymbolsDownloaded(string packageId, string version);
Task OnPackageUploaded(string packageId, string version);
Task OnSymbolsUploaded(string packageId, string version);
}
Methods
- CanDownloadPackage - Called before a package is downloaded. Return
falseto deny access to specific packages. - OnPackageDownloaded - Called after a package is successfully downloaded.
- OnSymbolsDownloaded - Called after symbol files are successfully downloaded.
- OnPackageUploaded - Called after a package is successfully uploaded.
- OnSymbolsUploaded - Called after symbol files are successfully uploaded.
Basic Implementation
Here's a simple implementation that logs all events:
using System.Threading.Tasks;
using AvantiPoint.Packages.Hosting;
using Microsoft.Extensions.Logging;
public class LoggingActionHandler : INuGetFeedActionHandler
{
private readonly ILogger<LoggingActionHandler> _logger;
public LoggingActionHandler(ILogger<LoggingActionHandler> logger)
{
_logger = logger;
}
public Task<bool> CanDownloadPackage(string packageId, string version)
{
_logger.LogInformation("Checking access to {PackageId} {Version}", packageId, version);
return Task.FromResult(true);
}
public Task OnPackageDownloaded(string packageId, string version)
{
_logger.LogInformation("Package downloaded: {PackageId} {Version}", packageId, version);
return Task.CompletedTask;
}
public Task OnSymbolsDownloaded(string packageId, string version)
{
_logger.LogInformation("Symbols downloaded: {PackageId} {Version}", packageId, version);
return Task.CompletedTask;
}
public Task OnPackageUploaded(string packageId, string version)
{
_logger.LogInformation("Package uploaded: {PackageId} {Version}", packageId, version);
return Task.CompletedTask;
}
public Task OnSymbolsUploaded(string packageId, string version)
{
_logger.LogInformation("Symbols uploaded: {PackageId} {Version}", packageId, version);
return Task.CompletedTask;
}
}
Accessing the Current User
If you've implemented authentication, you can access the authenticated user's claims in your action handler:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AvantiPoint.Packages.Hosting;
using Microsoft.AspNetCore.Http;
public class UserAwareActionHandler : INuGetFeedActionHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
public UserAwareActionHandler(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Task<bool> CanDownloadPackage(string packageId, string version)
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
{
// Allow anonymous access or deny based on your requirements
return Task.FromResult(true);
}
var email = user.FindFirst(ClaimTypes.Email)?.Value;
// Example: Check if user has access to this specific package
// This could query a database, check licenses, etc.
bool hasAccess = CheckUserLicense(email, packageId);
return Task.FromResult(hasAccess);
}
public Task OnPackageDownloaded(string packageId, string version)
{
var user = _httpContextAccessor.HttpContext?.User;
var username = user?.FindFirst(ClaimTypes.Name)?.Value ?? "anonymous";
// Record download metrics
RecordDownloadMetric(username, packageId, version);
return Task.CompletedTask;
}
// ... other methods
private bool CheckUserLicense(string email, string packageId)
{
// Your license checking logic here
return true;
}
private void RecordDownloadMetric(string username, string packageId, string version)
{
// Your metrics collection logic here
}
}
Email Notifications
Send emails when packages are uploaded:
using System.Threading.Tasks;
using AvantiPoint.Packages.Hosting;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System.Linq;
public class EmailNotificationHandler : INuGetFeedActionHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEmailService _emailService;
public EmailNotificationHandler(
IHttpContextAccessor httpContextAccessor,
IEmailService emailService)
{
_httpContextAccessor = httpContextAccessor;
_emailService = emailService;
}
public Task<bool> CanDownloadPackage(string packageId, string version)
{
return Task.FromResult(true);
}
public Task OnPackageDownloaded(string packageId, string version)
{
return Task.CompletedTask;
}
public Task OnSymbolsDownloaded(string packageId, string version)
{
return Task.CompletedTask;
}
public async Task OnPackageUploaded(string packageId, string version)
{
var user = _httpContextAccessor.HttpContext?.User;
var username = user?.FindFirst(ClaimTypes.Name)?.Value;
var email = user?.FindFirst(ClaimTypes.Email)?.Value;
if (!string.IsNullOrEmpty(email))
{
await _emailService.SendAsync(
to: email,
subject: $"Package Published: {packageId} {version}",
body: $"Your package {packageId} version {version} was successfully published."
);
}
}
public async Task OnSymbolsUploaded(string packageId, string version)
{
var user = _httpContextAccessor.HttpContext?.User;
var email = user?.FindFirst(ClaimTypes.Email)?.Value;
if (!string.IsNullOrEmpty(email))
{
await _emailService.SendAsync(
to: email,
subject: $"Symbols Published: {packageId} {version}",
body: $"Symbol files for {packageId} version {version} were successfully published."
);
}
}
}
Collecting Download Metrics
Track which packages are being downloaded and by whom:
using System.Threading.Tasks;
using AvantiPoint.Packages.Hosting;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
public class MetricsCollectionHandler : INuGetFeedActionHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMetricsRepository _metricsRepo;
public MetricsCollectionHandler(
IHttpContextAccessor httpContextAccessor,
IMetricsRepository metricsRepo)
{
_httpContextAccessor = httpContextAccessor;
_metricsRepo = metricsRepo;
}
public Task<bool> CanDownloadPackage(string packageId, string version)
{
return Task.FromResult(true);
}
public async Task OnPackageDownloaded(string packageId, string version)
{
var context = _httpContextAccessor.HttpContext;
var user = context?.User;
var ipAddress = context?.Connection?.RemoteIpAddress?.ToString();
await _metricsRepo.RecordDownloadAsync(new DownloadMetric
{
PackageId = packageId,
Version = version,
Username = user?.FindFirst(ClaimTypes.Name)?.Value,
Email = user?.FindFirst(ClaimTypes.Email)?.Value,
IpAddress = ipAddress,
Timestamp = DateTime.UtcNow
});
}
// ... other methods
}
Restricting Package Access
Control which packages users can download based on their license or subscription:
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AvantiPoint.Packages.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
public class LicenseBasedAccessHandler : INuGetFeedActionHandler
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly MyDbContext _db;
public LicenseBasedAccessHandler(
IHttpContextAccessor httpContextAccessor,
MyDbContext db)
{
_httpContextAccessor = httpContextAccessor;
_db = db;
}
public async Task<bool> CanDownloadPackage(string packageId, string version)
{
var user = _httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
{
return false; // Require authentication
}
var email = user.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrEmpty(email))
{
return false;
}
// Check if user has an active license for this package
var hasLicense = await _db.UserLicenses
.AnyAsync(x =>
x.UserEmail == email &&
x.PackageId == packageId &&
x.IsActive &&
x.ExpiresAt > DateTime.UtcNow);
return hasLicense;
}
// ... other methods return Task.CompletedTask
}
Registering Your Handler
In your Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Register HttpContextAccessor if your handler needs it
builder.Services.AddHttpContextAccessor();
// Register your action handler
builder.Services.AddScoped<INuGetFeedActionHandler, YourActionHandler>();
// Configure the package API
builder.Services.AddNuGetPackageApi(options =>
{
options.AddFileStorage();
options.AddSqliteDatabase("Sqlite");
});
Multiple Handlers
You can implement multiple interfaces or combine logic in a single handler. For complex scenarios, consider using a composite pattern to chain multiple handlers.
Important Notes
- The
ClaimsPrincipalavailable in theHttpContextis the one you returned fromIPackageAuthenticationService. - If no authentication service is registered, the user will be anonymous.
CanDownloadPackageis called before the package download begins. Returningfalsewill result in a 403 Forbidden response.- All other methods are called asynchronously after the operation completes.
- Exceptions thrown in handlers will be logged but won't fail the operation for the client.
See Also
- Authentication - Set up user authentication
- Sample: AuthenticatedFeed - Complete working example with callbacks