Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Build .NET backend services with clean architecture, dependency injection, Entity Framework, and ASP.NET Core patterns.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
assets/service-template.cs
1// Service Implementation Template for .NET 8+2// This template demonstrates best practices for building robust services34using System.Text.Json;5using FluentValidation;6using Microsoft.Extensions.Logging;7using Microsoft.Extensions.Options;89namespace YourNamespace.Application.Services;1011/// <summary>12/// Configuration options for the service13/// </summary>14public class ProductServiceOptions15{16public const string SectionName = "ProductService";1718public int DefaultPageSize { get; set; } = 50;19public int MaxPageSize { get; set; } = 200;20public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(15);21public bool EnableEnrichment { get; set; } = true;22}2324/// <summary>25/// Generic result type for operations that can fail26/// </summary>27public class Result<T>28{29public bool IsSuccess { get; }30public T? Value { get; }31public string? Error { get; }32public string? ErrorCode { get; }3334private Result(bool isSuccess, T? value, string? error, string? errorCode)35{36IsSuccess = isSuccess;37Value = value;38Error = error;39ErrorCode = errorCode;40}4142public static Result<T> Success(T value) => new(true, value, null, null);43public static Result<T> Failure(string error, string? code = null) => new(false, default, error, code);4445public Result<TNew> Map<TNew>(Func<T, TNew> mapper) =>46IsSuccess ? Result<TNew>.Success(mapper(Value!)) : Result<TNew>.Failure(Error!, ErrorCode);47}4849/// <summary>50/// Service interface - define the contract51/// </summary>52public interface IProductService53{54Task<Result<Product>> GetByIdAsync(string id, CancellationToken ct = default);55Task<Result<PagedResult<Product>>> SearchAsync(ProductSearchRequest request, CancellationToken ct = default);56Task<Result<Product>> CreateAsync(CreateProductRequest request, CancellationToken ct = default);57Task<Result<Product>> UpdateAsync(string id, UpdateProductRequest request, CancellationToken ct = default);58Task<Result<bool>> DeleteAsync(string id, CancellationToken ct = default);59}6061/// <summary>62/// Service implementation with full patterns63/// </summary>64public class ProductService : IProductService65{66private readonly IProductRepository _repository;67private readonly ICacheService _cache;68private readonly IValidator<CreateProductRequest> _createValidator;69private readonly IValidator<UpdateProductRequest> _updateValidator;70private readonly ILogger<ProductService> _logger;71private readonly ProductServiceOptions _options;7273public ProductService(74IProductRepository repository,75ICacheService cache,76IValidator<CreateProductRequest> createValidator,77IValidator<UpdateProductRequest> updateValidator,78ILogger<ProductService> logger,79IOptions<ProductServiceOptions> options)80{81_repository = repository ?? throw new ArgumentNullException(nameof(repository));82_cache = cache ?? throw new ArgumentNullException(nameof(cache));83_createValidator = createValidator ?? throw new ArgumentNullException(nameof(createValidator));84_updateValidator = updateValidator ?? throw new ArgumentNullException(nameof(updateValidator));85_logger = logger ?? throw new ArgumentNullException(nameof(logger));86_options = options?.Value ?? throw new ArgumentNullException(nameof(options));87}8889public async Task<Result<Product>> GetByIdAsync(string id, CancellationToken ct = default)90{91if (string.IsNullOrWhiteSpace(id))92return Result<Product>.Failure("Product ID is required", "INVALID_ID");9394try95{96// Try cache first97var cacheKey = GetCacheKey(id);98var cached = await _cache.GetAsync<Product>(cacheKey, ct);99100if (cached != null)101{102_logger.LogDebug("Cache hit for product {ProductId}", id);103return Result<Product>.Success(cached);104}105106// Fetch from repository107var product = await _repository.GetByIdAsync(id, ct);108109if (product == null)110{111_logger.LogWarning("Product not found: {ProductId}", id);112return Result<Product>.Failure($"Product '{id}' not found", "NOT_FOUND");113}114115// Populate cache116await _cache.SetAsync(cacheKey, product, _options.CacheDuration, ct);117118return Result<Product>.Success(product);119}120catch (Exception ex)121{122_logger.LogError(ex, "Error retrieving product {ProductId}", id);123return Result<Product>.Failure("An error occurred while retrieving the product", "INTERNAL_ERROR");124}125}126127public async Task<Result<PagedResult<Product>>> SearchAsync(128ProductSearchRequest request,129CancellationToken ct = default)130{131try132{133// Sanitize pagination134var pageSize = Math.Clamp(request.PageSize ?? _options.DefaultPageSize, 1, _options.MaxPageSize);135var page = Math.Max(request.Page ?? 1, 1);136137var sanitizedRequest = request with138{139PageSize = pageSize,140Page = page141};142143// Execute search144var (items, totalCount) = await _repository.SearchAsync(sanitizedRequest, ct);145146var result = new PagedResult<Product>147{148Items = items,149TotalCount = totalCount,150Page = page,151PageSize = pageSize,152TotalPages = (int)Math.Ceiling((double)totalCount / pageSize)153};154155return Result<PagedResult<Product>>.Success(result);156}157catch (Exception ex)158{159_logger.LogError(ex, "Error searching products with request {@Request}", request);160return Result<PagedResult<Product>>.Failure("An error occurred while searching products", "INTERNAL_ERROR");161}162}163164public async Task<Result<Product>> CreateAsync(CreateProductRequest request, CancellationToken ct = default)165{166// Validate167var validation = await _createValidator.ValidateAsync(request, ct);168if (!validation.IsValid)169{170var errors = string.Join("; ", validation.Errors.Select(e => e.ErrorMessage));171return Result<Product>.Failure(errors, "VALIDATION_ERROR");172}173174try175{176// Check for duplicates177var existing = await _repository.GetBySkuAsync(request.Sku, ct);178if (existing != null)179return Result<Product>.Failure($"Product with SKU '{request.Sku}' already exists", "DUPLICATE_SKU");180181// Create entity182var product = new Product183{184Id = Guid.NewGuid().ToString("N"),185Name = request.Name,186Sku = request.Sku,187Price = request.Price,188CategoryId = request.CategoryId,189CreatedAt = DateTime.UtcNow190};191192// Persist193var created = await _repository.CreateAsync(product, ct);194195_logger.LogInformation("Created product {ProductId} with SKU {Sku}", created.Id, created.Sku);196197return Result<Product>.Success(created);198}199catch (Exception ex)200{201_logger.LogError(ex, "Error creating product with SKU {Sku}", request.Sku);202return Result<Product>.Failure("An error occurred while creating the product", "INTERNAL_ERROR");203}204}205206public async Task<Result<Product>> UpdateAsync(207string id,208UpdateProductRequest request,209CancellationToken ct = default)210{211if (string.IsNullOrWhiteSpace(id))212return Result<Product>.Failure("Product ID is required", "INVALID_ID");213214// Validate215var validation = await _updateValidator.ValidateAsync(request, ct);216if (!validation.IsValid)217{218var errors = string.Join("; ", validation.Errors.Select(e => e.ErrorMessage));219return Result<Product>.Failure(errors, "VALIDATION_ERROR");220}221222try223{224// Fetch existing225var existing = await _repository.GetByIdAsync(id, ct);226if (existing == null)227return Result<Product>.Failure($"Product '{id}' not found", "NOT_FOUND");228229// Apply updates (only non-null values)230if (request.Name != null) existing.Name = request.Name;231if (request.Price.HasValue) existing.Price = request.Price.Value;232if (request.CategoryId.HasValue) existing.CategoryId = request.CategoryId.Value;233existing.UpdatedAt = DateTime.UtcNow;234235// Persist236var updated = await _repository.UpdateAsync(existing, ct);237238// Invalidate cache239await _cache.RemoveAsync(GetCacheKey(id), ct);240241_logger.LogInformation("Updated product {ProductId}", id);242243return Result<Product>.Success(updated);244}245catch (Exception ex)246{247_logger.LogError(ex, "Error updating product {ProductId}", id);248return Result<Product>.Failure("An error occurred while updating the product", "INTERNAL_ERROR");249}250}251252public async Task<Result<bool>> DeleteAsync(string id, CancellationToken ct = default)253{254if (string.IsNullOrWhiteSpace(id))255return Result<bool>.Failure("Product ID is required", "INVALID_ID");256257try258{259var existing = await _repository.GetByIdAsync(id, ct);260if (existing == null)261return Result<bool>.Failure($"Product '{id}' not found", "NOT_FOUND");262263// Soft delete264await _repository.DeleteAsync(id, ct);265266// Invalidate cache267await _cache.RemoveAsync(GetCacheKey(id), ct);268269_logger.LogInformation("Deleted product {ProductId}", id);270271return Result<bool>.Success(true);272}273catch (Exception ex)274{275_logger.LogError(ex, "Error deleting product {ProductId}", id);276return Result<bool>.Failure("An error occurred while deleting the product", "INTERNAL_ERROR");277}278}279280private static string GetCacheKey(string id) => $"product:{id}";281}282283// Supporting types284public record CreateProductRequest(string Name, string Sku, decimal Price, int CategoryId);285public record UpdateProductRequest(string? Name = null, decimal? Price = null, int? CategoryId = null);286public record ProductSearchRequest(287string? SearchTerm = null,288int? CategoryId = null,289decimal? MinPrice = null,290decimal? MaxPrice = null,291int? Page = null,292int? PageSize = null);293294public class PagedResult<T>295{296public IReadOnlyList<T> Items { get; init; } = Array.Empty<T>();297public int TotalCount { get; init; }298public int Page { get; init; }299public int PageSize { get; init; }300public int TotalPages { get; init; }301public bool HasNextPage => Page < TotalPages;302public bool HasPreviousPage => Page > 1;303}304305public class Product306{307public string Id { get; set; } = string.Empty;308public string Name { get; set; } = string.Empty;309public string Sku { get; set; } = string.Empty;310public decimal Price { get; set; }311public int CategoryId { get; set; }312public DateTime CreatedAt { get; set; }313public DateTime? UpdatedAt { get; set; }314}315316// Validators using FluentValidation317public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>318{319public CreateProductRequestValidator()320{321RuleFor(x => x.Name)322.NotEmpty().WithMessage("Name is required")323.MaximumLength(200).WithMessage("Name must not exceed 200 characters");324325RuleFor(x => x.Sku)326.NotEmpty().WithMessage("SKU is required")327.MaximumLength(50).WithMessage("SKU must not exceed 50 characters")328.Matches(@"^[A-Z0-9\-]+$").WithMessage("SKU must contain only uppercase letters, numbers, and hyphens");329330RuleFor(x => x.Price)331.GreaterThan(0).WithMessage("Price must be greater than 0");332333RuleFor(x => x.CategoryId)334.GreaterThan(0).WithMessage("Category is required");335}336}337