TinyCMS è un sistema di gestione dei contenuti leggero e modulare basato su .NET, progettato per essere facilmente estendibile e personalizzabile.
- Architettura modulare che consente di aggiungere o rimuovere funzionalità secondo necessità.
Richiesta paginata con filtro e ordinamento:
var page = new PageRequest(Page: 1, PageSize: 10);
Expression<Func<Post, bool>> filter = p => p.IsPublished && p.Title.Contains("dotnet");
Func<IQueryable<Post>, IOrderedQueryable<Post>> orderBy = q => q.OrderByDescending(p => p.PublishedAt);
var result = await _postRepository.GetPagedAsync(
pageRequest: page,
predicate: filter,
orderBy: orderBy,
selector: p => new PostDto { Id = p.Id, Title = p.Title, Slug = p.Slug },
includes: p => p.Author);Filtri dinamici più potenti
- Specification pattern (ISpecification) per incapsulare criteri, include, ordinamento.
- Dynamic LINQ / System.Linq.Dynamic.Core per espressioni testuali (es. sort by string).
- Costruzione di Expression a runtime usando Expression.AndAlso/OrElse per combinare più filtri.
Spec (esempio minimale)
public interface ISpecification<T>
{
Expression<Func<T,bool>>? Criteria { get; }
List<Expression<Func<T,object>>> Includes { get; }
Func<IQueryable<T>, IOrderedQueryable<T>>? OrderBy { get; }
}Poi un metodo ApplySpecification che trasforma IQueryable.
Unit of Work / Transactions
- Puoi usare direttamente DbContext come UoW: SaveChangesAsync() o SaveChanges in a transaction scope.
- Se vuoi separare, crea IUnitOfWork con CommitAsync() che chiama _db.SaveChangesAsync().
Registrazione in dependency injection
services.AddDbContext<MyDbContext>(options => options.UseSqlServer(connectionString));
services.AddScoped(typeof(IGenericRepository<,>), typeof(GenericRepository<,>));- Architettura: .NET 8, EF Core 8, Repository generico + (opzionale) UnitOfWork o affidarsi al DbContext, DTOs, AutoMapper, CQRS per operazioni complesse.
- Repository: operazioni CRUD asincrone, metodi per count/exists, GetPagedAsync con filtro dinamico (Expression<Func<T,bool>> o Specification), ordinamento dinamico, include dinamici, projection (Select).
- Paginazione: PageRequest + PagedResult.
- Filtri dinamici: Expression, Specification pattern, o costruzione di Expression a runtime (Dynamic LINQ).
- Best practices: non esporre IQueryable fuori dal livello repository (se non consapevole), usare projection per evitare overfetch, soft-delete e audit, caching se necessario.
- selector permette projection (DTO) usando Select -> molto importante per evitare N+1 o overfetch.
- predicate è Expression<Func<TEntity,bool>>: puoi costruirlo dinamicamente usando Expression combinators o Specification.
- orderBy è Func<IQueryable, IOrderedQueryable> per supportare ordini complessi, ad es. q => q.OrderByDescending(x=>x.UpdatedAt)
- Non esporre IQueryable oltre il repository a meno che il chiamante non conosca il DbContext (evita leakage di implementazione).
- Usare projection (Select) per DTO nelle query pubbliche.
- Implementare soft delete e campi di audit (CreatedAt, UpdatedAt, DeletedAt, CreatedBy).
- Aggiungere caching per endpoint costosi (Redis) e caching lato query.
- Test: unit test con repository mock o in-memory provider; integration test con dockerized DB.
- Migrations: EF Core migrations e strategie di deployment.
- Sicurezza: autenticazione (JWT / Identity) e autorizzazioni per l’admin.
- File/media: storage su blob (Azure, S3) e servizi per upload/thumbnail.
- Performance: evitare Include multipli non necessari, usare projection e paginazione server-side.
- Gestione contenuti: pagine, articoli, categorie, tag, media, blocchi componibili.
- Versioning e draft/publish workflow.
- Rich text editor + sanitizzazione HTML.
- Multilingua/localizzazione.
- Roles, permissions e audit per operazioni (who changed, what and when).
- Webhooks per integrazione esterne.
- GraphQL opzionale per flessibilità query.
- In .AddProjectPersistence ho usato services.AddFusionCache() e AddStackExchangeRedisCache(...) — FusionCache può essere configurata per usare la cache distribuita (IDistributedCache) per sincronizzare tra istanze.
- Se vuoi la cache distribuita, configura FusionCache per usare la Distributed cache (vedi documentazione FusionCache per l'opzione UseDistributedCache / DistributedCacheProvider).
- Invalidate esplicitamente la cache quando modifichi/soft-delete/restore/hard-delete le entità.
- Strategie di invalidazione: per semplicità rimuovere la chiave id-based dopo Update/SoftDelete/Remove. Per liste/pagine puoi usare chiavi derivate o prefissi e ClearByPrefix (se disponibile) o usare cache short TTL per pagine.
- Per contenuti CMS puoi usare TTL medio/breve (es. 30s–5min) e caching distribuito per ridurre incoerenze cross-instance.
- Se usi FusionCache con DistributedCache attivo, FusionCache gestirà fallback e sincronizzazione.
- Qui settiamo CreatedAt/UpdatedAt/DeletedAt nel repository; per assegnare CreatedBy/UpdatedBy/DeletedBy puoi popolare questi valori in service layer usando il current user id (es. IHttpContextAccessor).
- Considera l'uso di SaveChanges override nel DbContext per impostare automaticamente i campi di audit (più centralizzato).
- QueryOptions.IncludeDeleted ti permette di richiedere anche le entità soft-deleted quando necessario (per admin/restore).
- In alternativa alle filtrature nel repository, puoi definire HasQueryFilter(p => !p.IsDeleted) su ogni entità; se fai così, ricorda come bypassare il filtro per operazioni di restore (IgnoreQueryFilters()).
Recuperare pagina pubblicata usa:
repository.GetPagedAsync(..., predicate: p => p.IsPublished && !p.IsDeleted, selector: ...).Soft delete:
var p = await repo.GetByIdAsync(id); await repo.SoftDeleteAsync(p, currentUserId); await dbContext.SaveChangesAsync();