Mobility Pro

Plateforme d'offres d'emploi en ASP.NET Core : les entreprises publient des postes avec leurs prérequis, et chaque offre reçoit un score de compatibilité avec le profil du candidat et une distance par la route.

Présentation

Mobility Pro est une application web de mise en relation entre entreprises et candidats, écrite en C# avec ASP.NET Core 8. Je l'ai réalisée en deux mois et elle a été notée 20/20. Les entreprises décrivent leurs sites et publient des offres avec des prérequis chiffrés. Les membres renseignent leurs compétences et les postes qu'ils ont occupés. L'application croise les deux : chaque offre reçoit un score de compatibilité et une distance en voiture depuis l'adresse du candidat, qui servent à trier et filtrer la liste. Un panneau d'administration supervise les utilisateurs, les entreprises et les référentiels.

Un modèle centré sur les compétences

Le schéma tourne autour de TRequirementType, le référentiel des compétences. Une offre (TJobPost) porte des TJobPostRequirement : un niveau attendu de 0 à 10 et un nombre d'années d'expérience. Un membre déclare des TUserRequirement : une auto-évaluation sur la même échelle, une date de début et une date de fin facultative. Autour, j'ai modélisé les entreprises (TCompany), leurs sites (TSite), les métiers (TPoste), les types de contrat et l'historique professionnel (TExerce). La table UserCompanieAssociations relie un utilisateur à une ou plusieurs entreprises.

Les compétences d'un membre de démonstration : une auto-évaluation sur 10 et une période, ouverte (« jusqu'à ce jour ») ou close.

Les données passent par Entity Framework Core sur SQL Server. Program.cs enregistre deux contextes sur la même chaîne de connexion : AuthDbContext, qui hérite d'IdentityDbContext, et WebAppContext pour le métier. J'ai renommé les tables des utilisateurs et des rôles d'Identity en T_Users et T_Roles pour suivre la convention du reste du schéma.

Identity, rôles et pages par profil

L'authentification repose sur ASP.NET Identity, et ApplicationUser étend IdentityUser avec le nom, l'adresse, la date de naissance et les coordonnées géographiques. Les champs personnels portent [PersonalData], ce qui les inclut dans l'export de données proposé par Identity. La double authentification passe par les pages d'Identity : Login.cshtml.cs redirige vers LoginWith2fa quand le compte l'exige.

La page 2FA d'Identity avec une application d'authentification activée, sur un compte de démonstration qui a les rôles Admin et Membre : le menu affiche Administrator en plus de Métiers Exerces et Mes Compétences.

Au démarrage, Program.cs crée les rôles Admin, Manager, Enterprise et Membre, puis un compte administrateur s'il n'existe pas. L'inscription attribue Membre. Côté Razor Pages, un membre gère ses compétences (UserComp) et ses postes occupés (ListExerce). Un compte Enterprise gère les membres et les sites de son entreprise (ManageCompany) et publie des offres (AddJobPost). Depuis ViewPosts, il ouvre ManageRequirements pour définir les prérequis d'une offre, et la page refuse deux fois le même type de compétence. Les contrôleurs CRUD d'administration viennent du scaffolding MVC et sont protégés par [Authorize(Roles = "Admin")]. Un workflow GitHub Actions restaure et compile le projet en .NET 8 à chaque push et pull request sur master.

Dix points par prérequis

CalculateCompatibilityScore, dans TJobPostsController, compare chaque prérequis de l'offre à la compétence correspondante du membre. Cinq points mesurent l'écart de niveau, cinq autres l'expérience, au prorata si elle est insuffisante. Une compétence absente vaut zéro. Le total est ramené en pourcentage.

foreach (var requirement in jobPost.TJobPostRequirements)
{
    var userRequirement = userRequirements.FirstOrDefault(ur => ur.RequirementTypeId == requirement.RequirementTypeId);
    if (userRequirement == null) continue;
    double score = 5 - Math.Abs(userRequirement.PersonnalEvaluation - requirement.Evaluation);
    int years = CalculateYearsOfExperience(userRequirement.StartDate, userRequirement.EndDate);
    if (requirement.YearsOfExperience > 0)
        score += years >= requirement.YearsOfExperience ? 5 : (double)years / requirement.YearsOfExperience.Value * 5;
    totalScore += score;
}
return totalRequirements > 0 ? totalScore / (totalRequirements * 10) * 100 : 0;

J'ai déclaré CompatibilityScore en [NotMapped] : il dépend du membre connecté et n'a pas sa place en base.

Les offres vues par un membre de démonstration, triées par compatibilité décroissante au-dessus de 80 %, avec une distance vide car les sites de démonstration n'ont pas été géocodés (aucune clé Geoapify).

La distance par la route, pas à vol d'oiseau

Quand un utilisateur enregistre son adresse, la page de profil Manage/Index appelle l'API de géocodage de Geoapify et stocke latitude et longitude. Si l'adresse devient incomplète, les coordonnées sont effacées. Les sites créés depuis ManageCompany sont géocodés de la même façon.

Pour la liste des offres, CalculateDistancesAsync envoie une seule requête à l'API Route Matrix de Geoapify en mode drive : une source, le membre, et une cible par offre localisée. L'action List filtre ensuite par compétence, ville et poste, applique des bornes minimale et maximale sur la distance et la compatibilité, trie sur ces deux critères et pagine le résultat avec PagedResult<T>.

Ce que je reprendrais aujourd'hui

Le premier point est le contrôle d'accès. ManageCompany et ViewPosts vérifient l'appartenance à l'entreprise au chargement de la page, mais leurs handlers POST font confiance au CompanyId reçu dans la requête. AddJobPost et ManageRequirements ne la vérifient pas du tout, et ManageRequirements n'exige que le rôle Membre : seule la vue ViewPosts réserve aux comptes Enterprise le bouton qui y mène. OnPostRemoveExerceAsync supprime une expérience par son seul identifiant, et AdminController.Delete supprime un utilisateur sur une simple requête GET, sans validation anti-CSRF. DeleteRequirement montre pourtant la bonne pratique : il filtre sur UserId. Je centraliserais ces vérifications dans un handler d'autorisation par ressource.

Les prérequis d'une offre de Démo Logiciels, ouverts avec un compte qui n'a que le rôle Membre et n'appartient pas à l'entreprise : la page s'affiche avec ses boutons d'ajout et de suppression.

Les secrets ont aussi fini dans le dépôt : la clé Geoapify dans la configuration et dans une page, les identifiants du compte administrateur initial dans Program.cs. Je les passerais par les user-secrets et des variables d'environnement.

Côté performance, List charge toutes les offres puis filtre en mémoire, recalcule les distances de toutes les offres à chaque affichage et relit les compétences du membre pour chaque offre. Le score pénalise aussi un candidat plus fort que demandé, puisqu'il mesure un écart absolu, et sa part liée au niveau devient négative quand cet écart dépasse cinq. Un prérequis qui ne demande aucune expérience plafonne à cinq points sur dix.

Enfin, le workflow lance dotnet test alors que la solution ne contient aucun projet de test, et l'essentiel du code arrive dans un seul commit. Je découperais le travail en commits plus petits et j'écrirais des tests autour du score, la partie qui s'y prête le mieux.