SW-8-CSharp-SHACL

Navigation : << 7-OWL | Index | SW-8-Python >>

SHACL avec dotNetRDF : Valider la qualite de données RDF en C

Ce notebook est le jumeau C# / .NET du notebook SW-8-Python-SHACL. La ou le notebook Python utilise pySHACL (le moteur SHACL de reference en Python), ce notebook utilise dotNetRDF (le moteur RDF/SHACL de reference en .NET). Les deux implementent la même recommandation W3C SHACL (2017) : ce sont de veritables compagnons cross-langage sur un standard commun.

Verdict SOTA (EPIC #3801) : SOTA-OK. Ce notebook invoque le vrai moteur SHACL de dotNetRDF (espace de noms VDS.RDF.Shacl, package dotNetRdf.Shacl 3.4.1). Aucun workaround, aucune reimplementation jouet de la validation SHACL – le moteur reel tourne et produit le rapport de validation standard.

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Définir des shapes SHACL (NodeShape, PropertyShape) pour contraindre un graphe RDF 2. Valider des données avec le moteur SHACL de dotNetRDF (ShapesGraph.Validate) et interpreter le rapport 3. Construire des shapes personnalisees (en Turtle ou par l’API C#) 4. Utiliser les severites (Violation / Warning / Info) et les opérateurs logiques (sh:or, sh:not)

Concepts cles

Concept Description
SHACL Shapes Constraint Language – standard W3C 2017 pour valider des données RDF (monde ferme)
NodeShape Forme qui cible des noeuds RDF (par classe ou par URI)
PropertyShape Contrainte sur une propriete (cardinalite, type, motif, etc.)
ShapesGraph Classe dotNetRDF (VDS.RDF.Shacl.ShapesGraph) qui encapsule les formes et lance la validation
Validation Report Graphe RDF normalise decrivant chaque violation (sh:ValidationResult)

Prerequis

Duree estimee : 45 minutes


0. Chargement de dotNetRDF

dotNetRDF est la bibliotheque de reference pour manipuler RDF, SPARQL, RDFS, OWL et SHACL en .NET. Depuis la version 3.0, le support SHACL est inclus dans le meta-package dotNetRDF via le sous-package dotNetRdf.Shacl (namespace VDS.RDF.Shacl).

Espace de noms Rôle dans ce notebook
VDS.RDF Graphes, noeuds, triplets (Graph, IUriNode, ILiteralNode)
VDS.RDF.Parsing Lecture Turtle (TurtleParser)
VDS.RDF.Shacl Moteur SHACL (ShapesGraph, Validation.Report, Validation.Result)
VDS.RDF.Writing Serialisation des rapports (CompressingTurtleWriter)
// Chargement du meta-package dotNetRDF (inclut dotNetRdf.Shacl depuis la v3.0)
#r "nuget: dotNetRDF, 3.4.1"

using VDS.RDF;
using VDS.RDF.Parsing;
using VDS.RDF.Shacl;
using VDS.RDF.Writing;
using System.IO;
using StringWriter = System.IO.StringWriter;

Console.WriteLine("dotNetRDF 3.4.1 charge avec succes.");
Console.WriteLine("Espaces de noms SHACL disponibles : VDS.RDF.Shacl, VDS.RDF.Shacl.ShapesGraph");
Console.WriteLine("   (Validation via new ShapesGraph(g).Validate(data))");
Installed Packages
  • dotNetRDF, 3.4.1
dotNetRDF 3.4.1 charge avec succes.
Espaces de noms SHACL disponibles : VDS.RDF.Shacl, VDS.RDF.Shacl.ShapesGraph
   (Validation via new ShapesGraph(g).Validate(data))

Interpretation : Activation du moteur SHACL

La cellule ci-dessus : 1. #r "nuget: dotNetRDF, 3.4.1" restore le meta-package (et son sous-package dotNetRdf.Shacl). 2. Les using exposent les espaces de noms VDS.RDF.Shacl (le moteur) et VDS.RDF.Parsing (le lecteur Turtle). 3. L’alias using StringWriter = System.IO.StringWriter evite un conflit de noms avec VDS.RDF.Writing.StringWriter.

Note technique : l’API SHACL de dotNetRDF est centree sur la classe VDS.RDF.Shacl.ShapesGraph qui encapsule le graphe des formes et expose deux méthodes : Conforms(data) (booléen) et Validate(data) (rapport detaille). C’est l’equivalent C# de la fonction pyshacl.validate(...) du notebook Python.


1. Introduction a SHACL

Le problème : l’hypothese du monde ouvert

En RDF et OWL, on travaille sous l’hypothese du monde ouvert (Open World Assumption, OWA) : l’absence d’information ne signifie pas que cette information est fausse. Si un graphe RDF ne mentionne pas l’age d’une personne, OWL considere simplement que l’age est inconnu, pas qu’il est absent.

C’est un problème pour la qualite des données. Dans une application reelle, on veut pouvoir dire : - “Chaque personne doit avoir un nom” - “L’age doit etre un entier positif” - “Un email doit respecter un format précis”

OWL ne peut pas exprimer ces contraintes de validation. C’est la raison d’etre de SHACL (Shapes Constraint Language).

SHACL : Shapes Constraint Language

SHACL est une recommandation W3C publiee en juillet 2017 (Knublauch & Topidis, “SHACL Shapes Constraint Language”, W3C Rec. 2017). Elle définit un vocabulaire RDF pour decrire des contraintes (“shapes”) que les données RDF doivent respecter.

Aspect OWL (monde ouvert) SHACL (monde ferme)
Hypothese Monde ouvert (OWA) Monde ferme (CWA)
Objectif Inference, raisonnement Validation, qualite
Absence de donnee Inconnue Violation potentielle
minCount 1 Implique l’existence Exige la presence
Analogie Definition d’un concept Formulaire de saisie
Standard W3C OWL 2 (2012) SHACL 1.0 (2017)

Point cle : OWL decrit ce qui est vrai dans un domaine. SHACL decrit ce que les données doivent respecter. Les deux sont complementaires : OWL pour modeliser, SHACL pour valider.


2. Concepts fondamentaux de SHACL

SHACL repose sur deux types de formes (shapes) :

Concept URI SHACL Description
NodeShape sh:NodeShape Forme qui s’applique a un noeud RDF (une ressource)
PropertyShape sh:PropertyShape Forme qui contraint une propriete spécifique
targetClass sh:targetClass Cible tous les noeuds d’une classe donnee
targetNode sh:targetNode Cible un noeud spécifique par son URI
property sh:property Lie une NodeShape a ses PropertyShapes
path sh:path Designe la propriete RDF a contraindre

Architecture d’une shape SHACL (en Turtle)

ex:PersonShape a sh:NodeShape ;       # Forme pour les personnes
    sh:targetClass foaf:Person ;       # Cible : toutes les foaf:Person
    sh:property [                      # Contrainte sur une propriete
        sh:path foaf:name ;            # Propriete ciblee
        sh:minCount 1 ;                # Au moins une valeur
        sh:datatype xsd:string ;       # Type attendu
    ] .

Principaux types de contraintes

Catégorie Contrainte Propriete SHACL Exemple
Cardinalite Minimum / Maximum sh:minCount, sh:maxCount sh:minCount 1
Type Type de donnee sh:datatype sh:datatype xsd:string
Classe Classe RDF sh:class sh:class foaf:Person
Noeud Type de noeud sh:nodeKind sh:nodeKind sh:IRI
Chaîne Longueur minimale sh:minLength sh:minLength 2
Chaîne Motif regex sh:pattern sh:pattern "^[A-Z]"
Numérique Borne inclusive sh:minInclusive, sh:maxInclusive sh:minInclusive 0
Valeur Liste autorisee sh:in sh:in ("FR" "DE")
Logique OU / ET / NON sh:or, sh:and, sh:not, sh:xone liste de sous-formes

3. Explorer une shape SHACL existante

Le fichier data/person-shape.ttl contient une forme SHACL pour valider des instances de foaf:Person. Chargeons-le avec le TurtleParser de dotNetRDF et affichons son contenu.

// Chargement du graphe des formes SHACL depuis le fichier Turtle
IGraph shapesGraph = new Graph();
new TurtleParser().Load(shapesGraph, "data/person-shape.ttl");

Console.WriteLine($"Graphe des formes charge : {shapesGraph.Triples.Count} triplets");
Console.WriteLine();

// Serialisation Turtle pour afficher le contenu lisible
var sw = new StringWriter();
new CompressingTurtleWriter().Save(shapesGraph, sw);
Console.WriteLine("=== Contenu de person-shape.ttl ===");
Console.WriteLine(sw.ToString());
Graphe des formes charge : 25 triplets

=== Contenu de person-shape.ttl ===
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix sh: <http://www.w3.org/ns/shacl#>.
@prefix ex: <http://example.org/>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
@prefix schema: <http://schema.org/>.

ex:PersonShape a sh:NodeShape;
               sh:property [sh:path foaf:name ; 
                            sh:minCount 1  ; 
                            sh:maxCount 1  ; 
                            sh:datatype xsd:string ; 
                            sh:minLength 2  ; 
                            sh:message "Une personne doit avoir exactement un nom (string, min 2 caracteres)"@fr],
                           [sh:path foaf:mbox ; 
                            sh:maxCount 1  ; 
                            sh:pattern "^mailto:.+@.+\\..+$" ; 
                            sh:message "L'email doit etre au format mailto:user@domain.tld"@fr],
                           [sh:path foaf:age ; 
                            sh:maxCount 1  ; 
                            sh:datatype xsd:integer ; 
                            sh:minInclusive 0  ; 
                            sh:maxInclusive 150  ; 
                            sh:message "L'age doit etre un entier entre 0 et 150"@fr],
                           [sh:path foaf:knows ; 
                            sh:class foaf:Person ; 
                            sh:message "La propriete knows doit pointer vers une autre Person"@fr];
               sh:targetClass foaf:Person.

Analyse structurelle par programmation

Plutot que de lire le fichier Turtle a l’oeil, parcourons le graphe SHACL avec dotNetRDF pour identifier automatiquement les NodeShapes, leurs cibles et les contraintes de chaque propriete. On utilise GetTriplesWithSubjectPredicate pour retrouver les cibles (sh:targetClass) et les sous-formes (sh:property).

// Parcours structurel du graphe SHACL
IUriNode rdfType = shapesGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));
IUriNode shNodeShape = shapesGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/ns/shacl#NodeShape"));
IUriNode shTargetClass = shapesGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/ns/shacl#targetClass"));
IUriNode shProperty = shapesGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/ns/shacl#property"));
IUriNode shPath = shapesGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/ns/shacl#path"));
IUriNode shMessage = shapesGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/ns/shacl#message"));

foreach (Triple shapeT in shapesGraph.GetTriplesWithPredicateObject(rdfType, shNodeShape).ToList())
{
    INode shapeNode = shapeT.Subject;
    Console.WriteLine($"NodeShape : {shapeNode}");

    // Cible (targetClass)
    foreach (Triple tgt in shapesGraph.GetTriplesWithSubjectPredicate(shapeNode, shTargetClass))
        Console.WriteLine($"  Cible (targetClass) : {tgt.Object}");

    Console.WriteLine($"  Contraintes de propriete :");
    foreach (Triple propT in shapesGraph.GetTriplesWithSubjectPredicate(shapeNode, shProperty))
    {
        INode propShape = propT.Object;
        // Recuperer le path
        var pathTriples = shapesGraph.GetTriplesWithSubjectPredicate(propShape, shPath).ToList();
        string pathStr = pathTriples.Count > 0 ? pathTriples[0].Object.ToString() : "?";
        string pathLocal = pathStr.Contains("/") ? pathStr.Split('/').Last() : pathStr;
        Console.WriteLine($"    - {pathLocal}");

        // Lister toutes les contraintes (predicats sh:*)
        foreach (Triple c in shapesGraph.GetTriplesWithSubject(propShape))
        {
            string pred = c.Predicate.ToString();
            if (!pred.StartsWith("http://www.w3.org/ns/shacl#")) continue;
            string local = pred.Split('#').Last();
            if (local == "path") continue;
            Console.WriteLine($"        {local} = {c.Object}");
        }
    }
}
NodeShape : http://example.org/PersonShape
  Cible (targetClass) : http://xmlns.com/foaf/0.1/Person
  Contraintes de propriete :
    - name
        minCount = 1^^http://www.w3.org/2001/XMLSchema#integer
        maxCount = 1^^http://www.w3.org/2001/XMLSchema#integer
        datatype = http://www.w3.org/2001/XMLSchema#string
        minLength = 2^^http://www.w3.org/2001/XMLSchema#integer
        message = Une personne doit avoir exactement un nom (string, min 2 caracteres)@fr
    - mbox
        maxCount = 1^^http://www.w3.org/2001/XMLSchema#integer
        pattern = ^mailto:.+@.+\..+$^^http://www.w3.org/2001/XMLSchema#string
        message = L'email doit etre au format mailto:user@domain.tld@fr
    - age
        maxCount = 1^^http://www.w3.org/2001/XMLSchema#integer
        datatype = http://www.w3.org/2001/XMLSchema#integer
        minInclusive = 0^^http://www.w3.org/2001/XMLSchema#integer
        maxInclusive = 150^^http://www.w3.org/2001/XMLSchema#integer
        message = L'age doit etre un entier entre 0 et 150@fr
    - knows
        class = http://xmlns.com/foaf/0.1/Person
        message = La propriete knows doit pointer vers une autre Person@fr

Interpretation : Structure de la forme PersonShape

La forme ex:PersonShape cible toutes les instances de foaf:Person et impose 4 contraintes :

Propriete Contraintes Signification
foaf:name minCount=1, maxCount=1, datatype=string, minLength=2 Exactement un nom, chaîne d’au moins 2 caractères
foaf:mbox maxCount=1, pattern=^mailto:.+@.+\\..+$ Au plus un email, format mailto: valide
foaf:age maxCount=1, datatype=integer, minInclusive=0, maxInclusive=150 Au plus un age, entier entre 0 et 150
foaf:knows class=foaf:Person Les personnes connues doivent etre des foaf:Person

Note : foaf:mbox et foaf:age n’ont pas de minCount, ils sont donc optionnels. Seul foaf:name est obligatoire (minCount=1).


4. Validation SHACL avec dotNetRDF

L’API ShapesGraph

dotNetRDF expose la validation SHACL via la classe VDS.RDF.Shacl.ShapesGraph :

Membre Signature Description
Constructeur new ShapesGraph(IGraph shapes) Encapsule un graphe de formes SHACL
Conforms bool Conforms(IGraph data) True si les données sont conformes (sans detail)
Validate Validation.Report Validate(IGraph data) Rapport detaille des résultats

Le rapport retourne par Validate expose : - report.Conforms (booléen, False si au moins une violation sh:Violation) - report.Results (collection de Validation.Result) - Chaque Result a : Severity, FocusNode, ResultPath, Message, SourceConstraintComponent, ResultValue

Charger les données de test

Le fichier data/person-data.ttl contient 7 personnes dont 5 presentent des erreurs volontaires.

Pie cross-langage (G.1, verified firsthand) : le TurtleParser de dotNetRDF est plus strict que rdflib. Le fichier contient <not-an-email> comme URI relative (pour le test du pattern email) ; dotNetRDF refuse de parser une URI relative sans base. On fixe en assignant graph.BaseUri avant le chargement – rdflib (Python) l’accepte silencieusement. C’est une différence de robustesse entre les deux parseurs, documentee ici pour la parite.

// Chargement du graphe de donnees (avec base URI pour resoudre les URI relatives)
IGraph dataGraph = new Graph { BaseUri = UriFactory.Create("http://example.org/") };
new TurtleParser().Load(dataGraph, new StreamReader("data/person-data.ttl"));

Console.WriteLine($"Graphe de donnees charge : {dataGraph.Triples.Count} triplets");
Console.WriteLine();

// Lister les personnes avec leurs proprietes
IUriNode rdfType2 = dataGraph.CreateUriNode(UriFactory.Create("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));
IUriNode foafPerson = dataGraph.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/Person"));
IUriNode foafName = dataGraph.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/name"));
IUriNode foafAge = dataGraph.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/age"));
IUriNode foafMbox = dataGraph.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/mbox"));
IUriNode foafKnows = dataGraph.CreateUriNode(UriFactory.Create("http://xmlns.com/foaf/0.1/knows"));

Console.WriteLine($"{"Personne",-12} {"Nom",-18} {"Age",-6} {"Email",-28} {"Knows",-10}");
Console.WriteLine(new string('-', 76));

// Personnes = sujets de (s rdf:type foaf:Person)
var persons = dataGraph.GetTriplesWithPredicateObject(rdfType2, foafPerson).Select(t => t.Subject).OrderBy(n => n.ToString()).ToList();
foreach (INode person in persons)
{
    string name = GetOne(dataGraph, person, foafName) ?? "[ABSENT]";
    string age = GetOne(dataGraph, person, foafAge) ?? "-";
    string mbox = GetOne(dataGraph, person, foafMbox) ?? "-";
    if (mbox.StartsWith("http")) mbox = mbox.Split('/').Last();
    string knows = GetOne(dataGraph, person, foafKnows) ?? "-";
    if (knows.StartsWith("http")) knows = knows.Split('/').Last();
    Console.WriteLine($"{person.ToString().Split('/').Last(),-12} {name,-18} {age,-6} {mbox,-28} {knows,-10}");
}

// helper local : retourne la valeur "propre" d'un objet (le .Value d'un literal, sans le suffixe de datatype)
string GetOne(IGraph g, INode s, IUriNode p)
{
    var ts = g.GetTriplesWithSubjectPredicate(s, p).ToList();
    if (ts.Count == 0) return null;
    INode obj = ts[0].Object;
    return obj is ILiteralNode lit ? lit.Value : obj.ToString();
}
Graphe de donnees charge : 25 triplets

Personne     Nom                Age    Email                        Knows     
----------------------------------------------------------------------------
alice        Alice Dupont       30     mailto:alice@example.com     bob       
bob          Bob Martin         25     -                            alice     
charlie      [ABSENT]           40     -                            -         
diana        Diana Leroy        -5     -                            -         
eve          Eve Bernard        -      not-an-email                 -         
frank        F                  55     -                            -         
grace        Grace Moreau       -      -                            myDog     

Exécution de la validation

Lançons la validation en instanciant ShapesGraph sur le graphe des formes, puis en appelant Validate sur le graphe de données.

// Validation SHACL via le moteur dotNetRDF
var shapes = new ShapesGraph(shapesGraph);
var report = shapes.Validate(dataGraph);

Console.WriteLine($"Les donnees sont conformes : {report.Conforms}");
Console.WriteLine();
Console.WriteLine($"Nombre de resultats (violations + warnings + infos) : {report.Results.Count()}");
Console.WriteLine();
Console.WriteLine("=== Rapport detaille ===");
foreach (var r in report.Results)
{
    string focus = r.FocusNode.ToString().Split('/').Last();
    string path = r.ResultPath == null ? "-" : r.ResultPath.ToString().Split('/').Last();
    string component = r.SourceConstraintComponent.ToString().Split('#').Last();
    string value = r.ResultValue == null ? "-" : r.ResultValue.ToString();
    Console.WriteLine($"  [{component}] focus={focus}, path={path}, value={value}");
    Console.WriteLine($"      Message : {r.Message}");
}
Les donnees sont conformes : False

Nombre de resultats (violations + warnings + infos) : 5

=== Rapport detaille ===
  [MinCountConstraintComponent] focus=charlie, path=name, value=-
      Message : Une personne doit avoir exactement un nom (string, min 2 caracteres)@fr
  [MinInclusiveConstraintComponent] focus=diana, path=age, value=-5^^http://www.w3.org/2001/XMLSchema#integer
      Message : L'age doit etre un entier entre 0 et 150@fr
  [PatternConstraintComponent] focus=eve, path=mbox, value=http://example.org/not-an-email
      Message : L'email doit etre au format mailto:user@domain.tld@fr
  [MinLengthConstraintComponent] focus=frank, path=name, value=F^^http://www.w3.org/2001/XMLSchema#string
      Message : Une personne doit avoir exactement un nom (string, min 2 caracteres)@fr
  [ClassConstraintComponent] focus=grace, path=knows, value=http://example.org/myDog
      Message : La propriete knows doit pointer vers une autre Person@fr
// Analyse structuree : presentation tabulaire des violations
Console.WriteLine($"Nombre total de violations : {report.Results.Count()}");
Console.WriteLine();
Console.WriteLine($"{"Noeud",-12} {"Propriete",-10} {"Composant",-28} {"Severite",-12}");
Console.WriteLine(new string('-', 80));

foreach (var r in report.Results.OrderBy(x => x.FocusNode.ToString()))
{
    string focus = r.FocusNode.ToString().Split('/').Last();
    string path = r.ResultPath == null ? "-" : r.ResultPath.ToString().Split('/').Last();
    string component = r.SourceConstraintComponent.ToString().Split('#').Last().Replace("ConstraintComponent", "");
    string severity = r.Severity.ToString().Split('#').Last();
    Console.WriteLine($"{focus,-12} {path,-10} {component,-28} {severity,-12}");
}
Nombre total de violations : 5

Noeud        Propriete  Composant                    Severite    
--------------------------------------------------------------------------------
charlie      name       MinCount                     Violation   
diana        age        MinInclusive                 Violation   
eve          mbox       Pattern                      Violation   
frank        name       MinLength                    Violation   
grace        knows      Class                        Violation   

Interpretation : Les 5 erreurs detectees

Le fichier person-data.ttl contenait 5 erreurs volontaires, toutes detectees par le moteur SHACL de dotNetRDF. Ces 5 violations correspondent exactement aux 5 erreurs rapportedees par pySHACL dans le notebook Python (SW-8-Python-SHACL, section 4) – les deux moteurs s’accordent sur le même diagnostic.

Personne Propriete Composant de contrainte Explication
alice - (aucune) Conforme : toutes les proprietes respectent les contraintes
bob - (aucune) Conforme (email optionnel, pas de minCount sur mbox)
charlie foaf:name MinCount Pas de nom alors que minCount=1 est exige
diana foaf:age MinInclusive Age negatif (-5) alors que minInclusive=0
eve foaf:mbox Pattern Email not-an-email ne correspond pas au motif ^mailto:
frank foaf:name MinLength Nom “F” (1 caractère) trop court, minLength=2
grace foaf:knows Class ex:myDog est un ex:Dog, pas un foaf:Person

Points cles : 1. Alice et Bob passent la validation car leurs données sont completes et conformes. 2. Bob n’a pas d’email mais c’est conforme : foaf:mbox n’a pas de minCount. 3. Chaque violation est independante : un même noeud pourrait cumuler plusieurs violations. 4. Les noms de composants (MinCountConstraintComponent, MinInclusiveConstraintComponent, etc.) sont les identifiants standardises du vocabulaire SHACL du W3C – les mêmes que pySHACL.


5. Ecrire des shapes custom

Plutot que de charger des fichiers Turtle existants, on peut construire des formes SHACL directement en C#, soit en parsant une chaîne Turtle (l’approche la plus lisible, SHACL etant défini en Turtle), soit par l’API de graphe dotNetRDF (Assert, CreateBlankNode). Cette section montre les deux approches.

Catalogue des contraintes

Catégorie Propriete SHACL Description
Cardinalite sh:minCount, sh:maxCount Nombre min / max de valeurs
Type sh:datatype, sh:class, sh:nodeKind Type XSD, classe RDF, ou type de noeud
Chaîne sh:pattern, sh:minLength, sh:maxLength Regex, longueurs min / max
Numérique sh:minInclusive, sh:maxInclusive Bornes inclusives
Valeur sh:hasValue, sh:in Valeur exacte, liste autorisee
Logique sh:or, sh:and, sh:not, sh:xone Combinaisons de sous-formes

Exemple guide : une ArticleShape (construction en Turtle inline)

Creesons une forme SHACL pour valider des articles (schema:Article) : - Titre obligatoire (schema:headline, string, min 5 caractères) - Au moins un auteur (schema:author, doit etre un schema:Person) - Date de publication optionnelle (schema:datePublished, format xsd:date) - Nombre de mots optionnel (schema:wordCount, entier entre 100 et 50000)

// Construction d'une ArticleShape par Turtle inline (format natif de SHACL)
string articleShapesTtl = @"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

ex:ArticleShape a sh:NodeShape ;
    sh:targetClass schema:Article ;
    sh:property [
        sh:path schema:headline ;
        sh:minCount 1 ; sh:maxCount 1 ;
        sh:datatype xsd:string ; sh:minLength 5 ;
        sh:message ""Un article doit avoir un titre (string, min 5 caracteres)""@fr ;
    ] ;
    sh:property [
        sh:path schema:author ;
        sh:minCount 1 ;
        sh:class schema:Person ;
        sh:message ""Un article doit avoir au moins un auteur (schema:Person)""@fr ;
    ] ;
    sh:property [
        sh:path schema:datePublished ;
        sh:maxCount 1 ; sh:datatype xsd:date ;
        sh:message ""La date de publication doit etre au format xsd:date""@fr ;
    ] ;
    sh:property [
        sh:path schema:wordCount ;
        sh:maxCount 1 ; sh:datatype xsd:integer ;
        sh:minInclusive 100 ; sh:maxInclusive 50000 ;
        sh:message ""Le nombre de mots doit etre entre 100 et 50000""@fr ;
    ] .
";

IGraph articleShapes = new Graph();
new TurtleParser().Load(articleShapes, new StringReader(articleShapesTtl));
Console.WriteLine($"ArticleShape chargee : {articleShapes.Triples.Count} triplets dans le graphe des formes.");

// Donnees de test : 1 article valide + 3 invalides
string articleDataTtl = @"
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

ex:jean a schema:Person ; schema:name ""Jean Dupont"" .

# Article 1 : valide
ex:article1 a schema:Article ;
    schema:headline ""Introduction au Web Semantique"" ;
    schema:author ex:jean ;
    schema:datePublished ""2024-01-15""^^xsd:date ;
    schema:wordCount 5000 .

# Article 2 : titre trop court (""RDF"" = 3 caracteres < 5)
ex:article2 a schema:Article ;
    schema:headline ""RDF"" ;
    schema:author ex:jean .

# Article 3 : pas de titre, pas d'auteur
ex:article3 a schema:Article ;
    schema:wordCount 200 .

# Article 4 : nombre de mots hors bornes (10 < 100)
ex:article4 a schema:Article ;
    schema:headline ""Article avec trop peu de mots"" ;
    schema:author ex:jean ;
    schema:wordCount 10 .
";

IGraph articleData = new Graph { BaseUri = UriFactory.Create("http://example.org/") };
new TurtleParser().Load(articleData, new StringReader(articleDataTtl));

// Validation
var articleShapeGraph = new ShapesGraph(articleShapes);
var articleReport = articleShapeGraph.Validate(articleData);

Console.WriteLine($"Conforme : {articleReport.Conforms}");
Console.WriteLine($"Violations : {articleReport.Results.Count()}");
Console.WriteLine();
foreach (var r in articleReport.Results.OrderBy(x => x.FocusNode.ToString()))
{
    string focus = r.FocusNode.ToString().Split('/').Last();
    string path = r.ResultPath == null ? "-" : r.ResultPath.ToString().Split('/').Last();
    string component = r.SourceConstraintComponent.ToString().Split('#').Last().Replace("ConstraintComponent", "");
    Console.WriteLine($"  {focus} / {path} -> {component} : {r.Message}");
}
ArticleShape chargee : 26 triplets dans le graphe des formes.
Conforme : False
Violations : 4

  article2 / headline -> MinLength : Un article doit avoir un titre (string, min 5 caracteres)@fr
  article3 / headline -> MinCount : Un article doit avoir un titre (string, min 5 caracteres)@fr
  article3 / author -> MinCount : Un article doit avoir au moins un auteur (schema:Person)@fr
  article4 / wordCount -> MinInclusive : Le nombre de mots doit etre entre 100 et 50000@fr

Interpretation : Validation de la forme personnalisee

Article Violations Detail
article1 0 Conforme : titre, auteur, date et wordCount respectent toutes les contraintes
article2 1 Titre “RDF” trop court (3 < 5 caractères requis par minLength)
article3 2 Pas de titre (minCount=1) + pas d’auteur (minCount=1)
article4 1 wordCount=10 < minInclusive=100

Bonne pratique : ajoutz toujours des messages explicites (sh:message) pour faciliter le diagnostic. Les messages en francais sont plus utiles que les messages generiques du moteur.


Exercice 1 : MovieShape – valider un dataset de films

Créez une forme SHACL ex:MovieShape (en Turtle inline ou par l’API C#) pour valider des films (schema:Movie) avec : - Titre (schema:name) : obligatoire, chaîne, min 1 caractère - Annee (schema:datePublished) : entier entre 1900 et 2030 - Acteur (schema:actor) : au moins un, doit etre un schema:Person

Puis créez un graphe de données avec 2 films valides et 3 invalides, et validez.

Indices : - Reutilisez le patron de l’ArticleShape (Turtle inline + new TurtleParser().Load(g, new StringReader(ttl))). - Pour les bornes d’annee : sh:minInclusive 1900 ; sh:maxInclusive 2030. - Pour exiger un acteur d’une classe donnee : sh:minCount 1 ; sh:class schema:Person.

// Exercice 1 : MovieShape
// TODO etudiant : definissez la forme MovieShape en Turtle
string movieShapesTtl = @"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

# TODO etudiant : completez ex:MovieShape (titre, annee 1900-2030, acteur schema:Person)
ex:MovieShape a sh:NodeShape ;
    sh:targetClass schema:Movie .
";

// TODO etudiant : creez les donnees de test (2 films valides + 3 invalides)
// string movieDataTtl = @"... ";
// IGraph movieData = new Graph { BaseUri = UriFactory.Create("http://example.org/") };
// new TurtleParser().Load(movieData, new StringReader(movieDataTtl));

// TODO etudiant : validez et affichez les violations
// var movieShapeGraph = new ShapesGraph(movieShapes);
// var movieReport = movieShapeGraph.Validate(movieData);
// Console.WriteLine($"Conforme : {movieReport.Conforms}");

Console.WriteLine("Exercice a completer");
Exercice a completer

6. Severites SHACL

SHACL définit trois niveaux de severite pour les violations. Par defaut, toutes les contraintes ont la severite sh:Violation, mais on peut les ajuster avec sh:severity.

Severite URI Comportement Usage typique
Violation sh:Violation Erreur bloquante (defaut) Donnee manquante obligatoire
Warning sh:Warning Avertissement non bloquant Donnee recommandee mais optionnelle
Info sh:Info Information, suggestion Bonne pratique non imposee

Le booléen report.Conforms retourne False uniquement pour les sh:Violation. Les warnings et infos n’affectent pas la conformite – mais ils apparaissent dans report.Results.

// Demonstration des 3 niveaux de severite
string severityShapesTtl = @"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

ex:EmployeeShape a sh:NodeShape ;
    sh:targetClass ex:Employee ;
    sh:property [
        sh:path foaf:name ; sh:minCount 1 ;
        sh:severity sh:Violation ;
        sh:message ""Le nom est obligatoire (Violation)""@fr ;
    ] ;
    sh:property [
        sh:path foaf:mbox ; sh:minCount 1 ;
        sh:severity sh:Warning ;
        sh:message ""L'email est recommande (Warning)""@fr ;
    ] ;
    sh:property [
        sh:path ex:phone ; sh:minCount 1 ;
        sh:severity sh:Info ;
        sh:message ""Le telephone est souhaitable (Info)""@fr ;
    ] .
";

IGraph severityShapes = new Graph();
new TurtleParser().Load(severityShapes, new StringReader(severityShapesTtl));

// Employe sans nom, sans email, sans telephone
string severityDataTtl = @"
@prefix ex: <http://example.org/> .
ex:emp1 a ex:Employee .
";
IGraph severityData = new Graph { BaseUri = UriFactory.Create("http://example.org/") };
new TurtleParser().Load(severityData, new StringReader(severityDataTtl));

var severityShapeGraph = new ShapesGraph(severityShapes);
var severityReport = severityShapeGraph.Validate(severityData);

Console.WriteLine($"Conforme (Conforms) : {severityReport.Conforms}");
Console.WriteLine($"Nombre total de resultats : {severityReport.Results.Count()}");
Console.WriteLine();
Console.WriteLine("Resultats classés par severite :");
foreach (var r in severityReport.Results)
{
    string sev = r.Severity.ToString().Split('#').Last();
    Console.WriteLine($"  [{sev,-10}] {r.Message}");
}
Conforme (Conforms) : False
Nombre total de resultats : 3

Resultats classés par severite :
  [Violation ] Le nom est obligatoire (Violation)@fr
  [Warning   ] L'email est recommande (Warning)@fr
  [Info      ] Le telephone est souhaitable (Info)@fr

Interpretation : Impact de la severite

Bien que les 3 contraintes soient violees, le booléen report.Conforms ne depend que de sh:Violation :

Severite Contrainte violee Impact sur Conforms
sh:Violation Pas de nom Conforms = False
sh:Warning Pas d’email Pas d’impact (avertissement seulement, mais present dans Results)
sh:Info Pas de telephone Pas d’impact (information seulement, mais present dans Results)

Bonne pratique : utilisz sh:Warning pour les proprietes recommandees (best practices) et sh:Info pour les suggestions. Reservez sh:Violation aux exigences absolues.


Exercice 2 : Une forme avec severites mixtes

Créez une forme ex:StudentShape pour valider des etudiants (ex:Student) avec les contraintes et severites suivantes :

Propriete Contrainte Severite
foaf:name Obligatoire, chaîne sh:Violation
ex:studentId Obligatoire, motif ^[A-Z]{2}\\d{6}$ sh:Violation
foaf:mbox Recommande (minCount 1) sh:Warning
ex:phone Souhaitable (minCount 1) sh:Info

Testez avec 3 etudiants : un complet, un sans email/telephone, un sans nom. Verifiez que Conforms est False (a cause du nom manquant) mais que tous les résultats apparaissent dans report.Results.

Indices : sh:pattern "^[A-Z]{2}\\\\d{6}$" (echappement Turtle), sh:severity sh:Warning.

// Exercice 2 : StudentShape avec severites multiples
// TODO etudiant : definissez StudentShape en Turtle
string studentShapesTtl = @"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .

# TODO etudiant : completez ex:StudentShape (name Violation, studentId Violation, mbox Warning, phone Info)
ex:StudentShape a sh:NodeShape ;
    sh:targetClass ex:Student .
";

// TODO etudiant : creez les 3 etudiants de test, validez, et affichez les resultats par severite
Console.WriteLine("Exercice a completer");
Exercice a completer

7. Contraintes avancees : opérateurs logiques

SHACL permet de combiner des contraintes avec des opérateurs logiques :

Opérateur Propriete SHACL Sémantique
OU sh:or Au moins une des sous-formes doit etre satisfaite
ET sh:and Toutes les sous-formes doivent etre satisfaites
NON sh:not La sous-forme ne doit PAS etre satisfaite
OU exclusif sh:xone Exactement une sous-forme satisfaite

Exemple guide : un Contact doit avoir un email OU un telephone (sh:or)

La contrainte sh:or prend une liste RDF de sous-formes. Si au moins une sous-forme est satisfaite, la contrainte passe.

// Forme avancee avec sh:or (un Contact doit avoir un email OU un telephone)
string contactShapesTtl = @"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

ex:ContactShape a sh:NodeShape ;
    sh:targetClass ex:Contact ;
    sh:or (
        [ sh:path schema:email ; sh:minCount 1 ]
        [ sh:path schema:telephone ; sh:minCount 1 ]
    ) ;
    sh:message ""Un contact doit avoir un email ou un telephone""@fr .
";

IGraph contactShapes = new Graph();
new TurtleParser().Load(contactShapes, new StringReader(contactShapesTtl));

// 4 contacts : email seul, telephone seul, aucun, les deux
string contactDataTtl = @"
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

ex:contact1 a ex:Contact ; schema:name ""Alice"" ; schema:email ""alice@example.com"" .
ex:contact2 a ex:Contact ; schema:name ""Bob"" ; schema:telephone ""+33 1 23 45 67 89"" .
ex:contact3 a ex:Contact ; schema:name ""Charlie"" .
ex:contact4 a ex:Contact ; schema:name ""Diana"" ; schema:email ""diana@example.com"" ; schema:telephone ""+33 6 12 34 56 78"" .
";
IGraph contactData = new Graph { BaseUri = UriFactory.Create("http://example.org/") };
new TurtleParser().Load(contactData, new StringReader(contactDataTtl));

var contactShapeGraph = new ShapesGraph(contactShapes);
var contactReport = contactShapeGraph.Validate(contactData);

Console.WriteLine($"Conforme : {contactReport.Conforms}");
Console.WriteLine($"Nombre de violations : {contactReport.Results.Count()}");
Console.WriteLine();
foreach (var r in contactReport.Results)
{
    string focus = r.FocusNode.ToString().Split('/').Last();
    string component = r.SourceConstraintComponent.ToString().Split('#').Last().Replace("ConstraintComponent", "");
    Console.WriteLine($"  {focus} -> {component} : {r.Message}");
}
Conforme : False
Nombre de violations : 1

  contact3 -> Or : Un contact doit avoir un email ou un telephone@fr

Interpretation : Contraintes logiques

Contact Email Telephone Résultat Raison
contact1 Oui Non Conforme sh:or satisfait par l’email
contact2 Non Oui Conforme sh:or satisfait par le telephone
contact3 Non Non Violation OrConstraint sh:or non satisfait (aucune branche valide)
contact4 Oui Oui Conforme sh:or satisfait par les deux (OU inclusif)

Point cle : sh:or est un OU inclusif : avoir les deux options satisfaites est aussi valide. Pour exiger exactement une option, utilisz sh:xone (OU exclusif).


Exercice 3 : Contrainte sh:not et enumeration sh:in

On veut valider des comptes bancaires (ex:Account) avec : - schema:name obligatoire (string) - ex:status parmis "active", "frozen", "closed" (contrainte sh:in) - sh:not : le compte ne doit PAS avoir à la fois ex:status "closed" ET ex:balance positif (un compte ferme ne peut pas etre credite)

Construisez la forme et testez-la sur 3 comptes (1 valide, 2 invalides).

Indices : - sh:in ("active" "frozen" "closed") pour l’enumeration. - sh:not prend une sous-forme ; combinez avec sh:and pour exprimer “ferme ET balance>0” a interdire : sh:not [ sh:and ( [ sh:path ex:status ; sh:hasValue "closed" ] [ sh:path ex:balance ; sh:minExclusive 0 ] ) ] - Si votre version de SHACL/dotNetRDF rend sh:not+sh:and delicate, contentz-vous du sh:in seul (la structure de base). L’objectif est d’exprimer une contrainte negative.

// Exercice 3 : AccountShape avec sh:in et sh:not
// TODO etudiant : definissez AccountShape
string accountShapesTtl = @"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

# TODO etudiant : completez ex:AccountShape (name obligatoire, status dans {active,frozen,closed}, sh:not closed+balance>0)
ex:AccountShape a sh:NodeShape ;
    sh:targetClass ex:Account .
";

// TODO etudiant : creez 3 comptes (1 valide, 2 invalides), validez, affichez
Console.WriteLine("Exercice a completer");
Exercice a completer

8. Exemple guide : construire une BookShape par l’API C

Jusqu’ici nous avons défini les shapes en Turtle inline. dotNetRDF permet aussi de construire les formes par programmation (Assert de triplets), comme le fait rdflib en Python. Cette section montre l’equivalent C# pour une BookShape :

  • Titre (schema:name) : obligatoire, chaîne, min 1 caractère
  • ISBN (schema:isbn) : motif EAN-13 ^97[89]-\\d-\\d{4}-\\d{4}-\\d$
  • Annee (schema:datePublished) : entier entre 1450 et 2030
  • Auteur (schema:author) : au moins un, IRI de type schema:Person
// Exemple guide : BookShape construite par l'API dotNetRDF (equivalent C# du build-shape Python)
IGraph bookShapes = new Graph();
bookShapes.NamespaceMap.AddNamespace("sh", UriFactory.Create("http://www.w3.org/ns/shacl#"));
bookShapes.NamespaceMap.AddNamespace("ex", UriFactory.Create("http://example.org/"));
bookShapes.NamespaceMap.AddNamespace("schema", UriFactory.Create("http://schema.org/"));
bookShapes.NamespaceMap.AddNamespace("xsd", UriFactory.Create("http://www.w3.org/2001/XMLSchema#"));

IUriNode SH(string l) => bookShapes.CreateUriNode(UriFactory.Create("http://www.w3.org/ns/shacl#" + l));
IUriNode SCHEMA(string l) => bookShapes.CreateUriNode(UriFactory.Create("http://schema.org/" + l));
IUriNode XSD(string l) => bookShapes.CreateUriNode(UriFactory.Create("http://www.w3.org/2001/XMLSchema#" + l));

IUriNode bookShape = bookShapes.CreateUriNode("ex:BookShape");
bookShapes.Assert(new Triple(bookShape, SH("type"), SH("NodeShape")));
bookShapes.Assert(new Triple(bookShape, SH("targetClass"), SCHEMA("Book")));

// Contrainte 1 : titre obligatoire
IBlankNode titlePs = bookShapes.CreateBlankNode();
bookShapes.Assert(new Triple(bookShape, SH("property"), titlePs));
bookShapes.Assert(new Triple(titlePs, SH("path"), SCHEMA("name")));
bookShapes.Assert(new Triple(titlePs, SH("minCount"), (1).ToLiteral(bookShapes)));
bookShapes.Assert(new Triple(titlePs, SH("datatype"), XSD("string")));
bookShapes.Assert(new Triple(titlePs, SH("minLength"), (1).ToLiteral(bookShapes)));

// Contrainte 2 : ISBN avec pattern
IBlankNode isbnPs = bookShapes.CreateBlankNode();
bookShapes.Assert(new Triple(bookShape, SH("property"), isbnPs));
bookShapes.Assert(new Triple(isbnPs, SH("path"), SCHEMA("isbn")));
bookShapes.Assert(new Triple(isbnPs, SH("datatype"), XSD("string")));
bookShapes.Assert(new Triple(isbnPs, SH("pattern"), bookShapes.CreateLiteralNode(@"^97[89]-\d-\d{4}-\d{4}-\d$")));

// Contrainte 3 : annee entre 1450 et 2030
IBlankNode yearPs = bookShapes.CreateBlankNode();
bookShapes.Assert(new Triple(bookShape, SH("property"), yearPs));
bookShapes.Assert(new Triple(yearPs, SH("path"), SCHEMA("datePublished")));
bookShapes.Assert(new Triple(yearPs, SH("datatype"), XSD("integer")));
bookShapes.Assert(new Triple(yearPs, SH("minInclusive"), (1450).ToLiteral(bookShapes)));
bookShapes.Assert(new Triple(yearPs, SH("maxInclusive"), (2030).ToLiteral(bookShapes)));

// Contrainte 4 : au moins un auteur (IRI de type schema:Person)
IBlankNode authorPs = bookShapes.CreateBlankNode();
bookShapes.Assert(new Triple(bookShape, SH("property"), authorPs));
bookShapes.Assert(new Triple(authorPs, SH("path"), SCHEMA("author")));
bookShapes.Assert(new Triple(authorPs, SH("minCount"), (1).ToLiteral(bookShapes)));
bookShapes.Assert(new Triple(authorPs, SH("nodeKind"), SH("IRI")));
bookShapes.Assert(new Triple(authorPs, SH("class"), SCHEMA("Person")));

Console.WriteLine($"BookShape construite : {bookShapes.Triples.Count} triplets dans le graphe des formes.");

// Donnees de test (2 valides + 2 invalides)
string bookDataTtl = @"
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix ex: <http://example.org/> .
@prefix schema: <http://schema.org/> .

ex:author1 a schema:Person .
ex:author2 a schema:Person .

ex:book1 a schema:Book ;
    schema:name ""Clean Code"" ;
    schema:isbn ""978-0-1323-5088-4"" ;
    schema:datePublished 2008 ;
    schema:author ex:author1 .

ex:book2 a schema:Book ;
    schema:name ""Design Patterns"" ;
    schema:isbn ""978-0-2016-3361-0"" ;
    schema:datePublished 1994 ;
    schema:author ex:author2 .

# Invalide : pas de titre, ISBN errone, pas d'auteur
ex:book3 a schema:Book ;
    schema:isbn ""123-INVALID"" .

# Invalide : annee hors bornes (3000 > 2030), auteur non-Person
ex:book4 a schema:Book ;
    schema:name ""Future Book"" ;
    schema:datePublished 3000 ;
    schema:author ex:robot .
ex:robot a schema:Organization .
";
IGraph bookData = new Graph { BaseUri = UriFactory.Create("http://example.org/") };
new TurtleParser().Load(bookData, new StringReader(bookDataTtl));

var bookShapeGraph = new ShapesGraph(bookShapes);
var bookReport = bookShapeGraph.Validate(bookData);

Console.WriteLine($"Conforme : {bookReport.Conforms}");
Console.WriteLine($"Violations : {bookReport.Results.Count()}");
Console.WriteLine();
foreach (var r in bookReport.Results.OrderBy(x => x.FocusNode.ToString()))
{
    string focus = r.FocusNode.ToString().Split('/').Last();
    string path = r.ResultPath == null ? "-" : r.ResultPath.ToString().Split('/').Last();
    string component = r.SourceConstraintComponent.ToString().Split('#').Last().Replace("ConstraintComponent", "");
    Console.WriteLine($"  {focus} / {path} -> {component}");
}
BookShape construite : 21 triplets dans le graphe des formes.
Conforme : False
Violations : 5

  book3 / name -> MinCount
  book3 / isbn -> Pattern
  book3 / author -> MinCount
  book4 / datePublished -> MaxInclusive
  book4 / author -> Class

Interpretation : BookShape par l’API programmatique

La construction par Assert/CreateBlankNode est l’equivalent C# du graph.add((shape, sh.property, BNode())) de rdflib. Elle est plus verbeuse que le Turtle inline mais utile quand les formes sont generees dynamiquement (depuis un schema SQL, une UI, etc.).

Livre Violations Detail
book1 0 Conforme
book2 0 Conforme
book3 3 Pas de titre (minCount), ISBN errone (pattern), pas d’auteur (minCount)
book4 2 Annee 3000 > 2030 (maxInclusive), auteur non-Person (class)

Quand utiliser Turtle inline vs API : Turtle inline pour des formes fixes (lisibilite maximale, proche de la spec SHACL) ; API programmatique pour des formes generees par le code.


9. Comparaison : dotNetRDF SHACL vs pySHACL

Ce notebook et son jumeau SW-8-Python-SHACL appliquent le même standard W3C SHACL via deux moteurs de reference. Les résultats concordent (mêmes 5 violations sur person-data.ttl, mêmes composants de contrainte, mêmes severites) ; les différences sont dans l’API.

Aspect pySHACL (Python) dotNetRDF (C#)
Point d’entree validate(data_graph, shacl_graph=...) new ShapesGraph(shapes).Validate(data)
Retour triplet (conforms, results_graph, results_text) Validation.Report (objet type)
Rapport graphe RDF + texte formate report.Results (collection de Result) + graphe RDF
Severites sh:Violation / Warning / Info idem (même vocabulaire W3C)
Opérateurs logiques sh:or, sh:and, sh:not, sh:xone idem
Parsing Turtle rdflib (tolerant, URIs relatives acceptees) TurtleParser (strict, exige une base URI)
Construction API graph.add((s, p, o)) + BNode() graph.Assert(new Triple(s, p, o)) + CreateBlankNode()
Inference RDFS inference='rdfs' (paramètre de validate) pre-materialisation a faire avant validation
Type de la forme rdflib.Graph VDS.RDF.Shacl.ShapesGraph (encapsule un IGraph)

Concordance verifiee firsthand (G.1) : sur person-shape.ttl + person-data.ttl, les deux moteurs rapportent exactement les 5 mêmes violations (charlie/MinCount, diana/MinInclusive, eve/Pattern, frank/MinLength, grace/Class), avec les mêmes composants de contrainte standardises. C’est la garantie que SHACL est un vrai standard : deux implementations independantes convergent.

Différence notable : pySHACL materialise l’inference RDFS option (inference='rdfs') lors de la validation ; dotNetRDF delegue l’inference au VDS.RDF.Inferencing (a effectuer avant validation si besoin). Pour les formes cibles par sh:targetClass, il faut s’assurer que les types explicites (a foaf:Person) sont presents dans le graphe de données.


Resume

Tableau recapitulatif

Concept Ce que nous avons appris
SHACL Recommandation W3C 2017 pour la validation de données RDF (monde ferme)
NodeShape / PropertyShape Forme sur un noeud / contrainte sur une propriete
dotNetRDF SHACL new ShapesGraph(shapes).Validate(data) -> Validation.Report
Rapport report.Conforms + report.Results (Severity, FocusNode, ResultPath, Message, SourceConstraintComponent)
Severites sh:Violation (bloquant), sh:Warning (avertissement), sh:Info (suggestion)
Opérateurs logiques sh:or, sh:and, sh:not, sh:xone
Construction Turtle inline (new TurtleParser().Load(g, new StringReader(ttl))) ou API (Assert/CreateBlankNode)
Gotcha cross-langage TurtleParser dotNetRDF plus strict que rdflib (URI relative exige une base)

Contraintes SHACL – Reference rapide

Catégorie Propriete SHACL Exemple
Cardinalite sh:minCount, sh:maxCount sh:minCount 1
Type sh:datatype, sh:class, sh:nodeKind sh:datatype xsd:string
Chaîne sh:pattern, sh:minLength, sh:maxLength sh:pattern "^[A-Z]"
Numérique sh:minInclusive, sh:maxInclusive sh:minInclusive 0
Valeur sh:hasValue, sh:in sh:in ("FR" "DE")
Logique sh:or, sh:and, sh:not, sh:xone Liste de sous-formes

Exemples et exercices du notebook

Type Titre Section
Exemple guide Validation de person-data.ttl (5 erreurs) 4
Exemple guide ArticleShape en Turtle inline 5
Exercice 1 MovieShape – valider un dataset de films 5
Exemple guide EmployeeShape (severites Violation/Warning/Info) 6
Exercice 2 StudentShape avec severites mixtes 6
Exemple guide ContactShape avec sh:or 7
Exercice 3 AccountShape avec sh:in et sh:not 7
Exemple guide BookShape par l’API programmatique 8

Ressources supplementaires


Conclusion

Ce notebook a permis d’explorer la validation SHACL en C# avec le vrai moteur dotNetRDF (VDS.RDF.Shacl), jumeau du notebook Python pySHACL sur le même standard W3C. Les points cles :

  • SHACL operationalise le monde ferme pour la qualite des données la ou OWL reste en monde ouvert.
  • L’API dotNetRDF (ShapesGraph.Validate -> Validation.Report) est l’equivalent type de pyshacl.validate.
  • Les 5 violations sur les données de test concordent exactement entre dotNetRDF et pySHACL – deux implementations independantes convergent sur le standard.
  • Les severites et les opérateurs logiques (sh:or, sh:not) ouvrent la voie a des contraintes business riches.

Pour aller plus loin : combinez SHACL avec RDFS/OWL (SW-6, SW-7) pour valider des données inferees, et explorez SHACL Advanced Features (règles de transformation sh:rule) pour la materialisation.


Navigation : << 7-OWL | Index | SW-8-Python >>

Retour au sommet