Intégration de la couche d'accès aux données
Pour la persistance des données, le projet s'appuie sur FreeSQL. Contrairement aux frameworks nécessitant des migrations explicites, cette bilbiothèque active la synchronisation automatique du schéma en mode développement, ce qui fluidifie le cycle de prototypage.
La configuration s'effectue via une méthode d'extension dédiée au conteneur d'injection de dépendances :
using FreeSql;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace InfrastructureBlog.Extensions
{
public static class ConfigurationDonnees
{
public static IServiceCollection ActiverFreeSql(this IServiceCollection services, IConfiguration config)
{
var instanceBase = new FreeSqlBuilder()
.UseConnectionString(DataType.Sqlite, config.GetConnectionString("CheminSqlite"))
.UseAutoSyncStructure(true)
.Build();
services.AddSingleton(instanceBase);
services.AddFreeRepository(null, typeof(ConfigurationDonnees).Assembly);
return services;
}
}
}
L'activation dans le pipeline d'application se résume à un appel unique :
builder.Services.ActiverFreeSql(builder.Configuration);
Une fois enregistré, le générique IBaseRepository<T> est directement injectable, offrant une API relationnelle prête à l'emploi sans boilerplate.
Architecture de la couche métier
Afin d'isoler la logique applicative des contrôleurs HTTP, une classe de service orchestre les requêtes et les règles métier. L'injection des référentiels garantit un couplage faible.
public class GestionnaireArticles
{
private readonly IBaseRepository<Article> _repoContenu;
private readonly IBaseRepository<Categorie> _repoTaxonomie;
public GestionnaireArticles(IBaseRepository<Article> contenu, IBaseRepository<Categorie> taxonomie)
{
_repoContenu = contenu;
_repoTaxonomie = taxonomie;
}
}
Gestion des paramètres de requête et pagination
L'affichage exhaustif des entrées entraîne rapidement des problèmes de performance et d'ergonomie. La pagination est traitée via le package X.PagedList, qui standadrise la conversion de collections brutes en objets typés contenant les métadonnées de navigation et les sous-ensembles de données.
Pour éviter la multiplication des arguments dans les signatures de méthodes, une stratégie de regroupement dans des objets de transfert est appliquée. Une classe de base capture les contraintes transversales :
public abstract class ParametresNavigation
{
private const int LimiteSuperieure = 50;
private int _tailleLot = 15;
public int TailleLot
{
get => _tailleLot;
set => _tailleLot = Math.Clamp(value, 1, LimiteSuperieure);
}
public int IndexPage { get; set; } = 1;
public string? TermeRecherche { get; set; }
public string? ExpressionTri { get; set; }
}
Une spécialisation hérite de cette structure pour y ajouter les critères propres aux publications :
public class CriteresRechercheArticles : ParametresNavigation
{
public bool VisibiliseUniquementPublie { get; set; } = true;
public int IdentifiantCategorie { get; set; } = 0;
public string? StatutExterne { get; set; }
public override string ExpressionTri { get; set; } = "-DateModification";
}
Implémentation de la logique de filtrage
La méthode principale du service construit dynamiquement l'arbre d'expressions LINQ. Le moteur de traduction convertira la requête finale en instructions SQL optimisées avant l'exécution matérielle.
public IPagedList<Article> ObtenirParLots(CriteresRechercheArticles criteres)
{
var jeuDonnees = _repoContenu.Select;
if (criteres.VisibiliseUniquementPublie)
jeuDonnees = jeuDonnees.Where(x => x.EstPublie);
if (criteres.IdentifiantCategorie > 0)
jeuDonnees = jeuDonnees.Where(x => x.CategorieId == criteres.IdentifiantCategorie);
if (!string.IsNullOrWhiteSpace(criteres.TermeRecherche))
jeuDonnees = jeuDonnees.Where(x => x.Titre.Contains(criteres.TermeRecherche) || x.Resume.Contains(criteres.TermeRecherche));
if (!string.IsNullOrWhiteSpace(criteres.ExpressionTri))
{
bool ascendant = !criteres.ExpressionTri.StartsWith("-");
string colonne = criteres.ExpressionTri.TrimStart('-');
jeuDonnees = jeuDonnees.OrderByPropriete(colonne, ascendant);
}
return jeuDonnees
.Include(x => x.Categorie)
.ToList()
.ToPagedList(criteres.IndexPage, criteres.TailleLot);
}
Orchestration du Contrôleur MVC
Le contrôleur agit comme coordinateur. Il délègue l'extraction des données au service, enrichit le contexte (comme la liste hiérarchique des catégories) et transmet le résultat à la vue via un modèle dédié. Cette séparation permet de conserver une API REST propre tout en gérant le rendu serveur.
public IActionResult AfficherListe(int idCat = 0, int pg = 1, int lot = 10)
{
var taxonomie = _repoTaxonomie
.Where(c => c.EstVisible)
.IncludeMany(c => c.Contenus)
.ToList();
taxonomie.Insert(0, new Categorie { Id = 0, Nom = "Tous", Contenus = _repoContenu.Select.ToList() });
var vueModele = new PresentationListe
{
CategorieActive = idCat == 0 ? taxonomie.First() : taxonomie.First(c => c.Id == idCat),
IdCategorieSelectionnee = idCat,
EnsembleCategorie = taxonomie,
Publications = _service.ObtenirParLots(new CriteresRechercheArticles
{
IdentifiantCategorie = idCat,
IndexPage = pg,
TailleLot = lot,
VisibiliseUniquementPublie = true
})
};
return View(vueModele);
}
Composition du ViewModel et rendu Razor
Le ViewModel centralise les données de présentation sans exposer les entités de domaine. Il sert de contrat strict entre le contrôleur et le template d'affichage.
namespace Presentation.Web.ModelesVues
{
public class PresentationListe
{
public Categorie CategorieActive { get; init; } = null!;
public int IdCategorieSelectionnee { get; init; }
public IPagedList<Article> Publications { get; init; } = null!;
public IList<Categorie> EnsembleCategorie { get; init; } = new List<Categorie>();
}
}
Le fichier Views/Blog/AfficherListe.cshtml utilise ce modèle pour itérer sur les publications. Le composant de pagination génère automatiquement la structure HTML compatible avec les frameworks CSS modernes, en préservant les filtres actifs dans les URLs.
@model Presentation.Web.ModelesVues.PresentationListe
@using X.PagedList.Mvc.Core
@{ ViewData["Titre"] = "Catalogue des Publications"; }
<div class="conteneur-grille">
@foreach (var article in Model.Publications)
{
<article class="carte-entree mb-3">
<header class="en-tete-carte">
<span class="badge-categorie">@article.Categorie.Nom</span>
</header>
<section class="corps-carte">
<h2 class="titre-article">@article.Titre</h2>
<p class="texte-resume">@article.Resume</p>
<a class="bouton-lire" asp-controller="Blog" asp-action="Details" asp-route-identifiant="@article.Id">
Lire la suite
</a>
</section>
</article>
}
</div>
<nav aria-label="Navigation des pages">
@Html.PagedListPager(Model.Publications, page => Url.Action(
"AfficherListe", "Blog", new { pg = page, idCat = Model.IdCategorieSelectionnee }),
new PagedListRenderOptions
{
LiElementClasses = new[] { "element-page" },
PageClasses = new[] { "lien-page" },
UlElementClasses = new[] { "liste-pagination centrage" }
})
</nav>
L'architecture combine ainsi la flexibilité du rendu serveur avec une modularité stricte. La logique de tri, de filtrage et de pagination reste encapsulée dans la couche service, tandis que le contrôleur orchestre le contexte et que la vue se concentre exclusivement sur la restitution structurelle.