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
Notebook SW-7-CSharp-OWL complete (ontologies, monde ouvert)
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).
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 TurtleIGraph shapesGraph =newGraph();newTurtleParser().Load(shapesGraph,"data/person-shape.ttl");Console.WriteLine($"Graphe des formes charge : {shapesGraph.Triples.Count} triplets");Console.WriteLine();// Serialisation Turtle pour afficher le contenu lisiblevar sw =newStringWriter();newCompressingTurtleWriter().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 SHACLIUriNode 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 pathvar 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 :
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/")};newTurtleParser().Load(dataGraph,newStreamReader("data/person-data.ttl"));Console.WriteLine($"Graphe de donnees charge : {dataGraph.Triples.Count} triplets");Console.WriteLine();// Lister les personnes avec leurs proprietesIUriNode 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(newstring('-',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)stringGetOne(IGraph g, INode s, IUriNode p){var ts = g.GetTriplesWithSubjectPredicate(s, p).ToList();if(ts.Count==0)returnnull; 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 dotNetRDFvar shapes =newShapesGraph(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 violationsConsole.WriteLine($"Nombre total de violations : {report.Results.Count()}");Console.WriteLine();Console.WriteLine($"{"Noeud",-12} {"Propriete",-10} {"Composant",-28} {"Severite",-12}");Console.WriteLine(newstring('-',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 =newGraph();newTurtleParser().Load(articleShapes,newStringReader(articleShapesTtl));Console.WriteLine($"ArticleShape chargee : {articleShapes.Triples.Count} triplets dans le graphe des formes.");// Donnees de test : 1 article valide + 3 invalidesstring 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: valideex: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'auteurex: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/")};newTurtleParser().Load(articleData,newStringReader(articleDataTtl));// Validationvar articleShapeGraph =newShapesGraph(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 Turtlestring 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 Falseuniquement pour les sh:Violation. Les warnings et infos n’affectent pas la conformite – mais ils apparaissent dans report.Results.
// Demonstration des 3 niveaux de severitestring 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 =newGraph();newTurtleParser().Load(severityShapes,newStringReader(severityShapesTtl));// Employe sans nom, sans email, sans telephonestring severityDataTtl = @"@prefix ex:<http://example.org/> .ex:emp1 a ex:Employee .";IGraph severityData =new Graph { BaseUri = UriFactory.Create("http://example.org/")};newTurtleParser().Load(severityData,newStringReader(severityDataTtl));var severityShapeGraph =newShapesGraph(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 Turtlestring 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 severiteConsole.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 =newGraph();newTurtleParser().Load(contactShapes,newStringReader(contactShapesTtl));// 4 contacts : email seul, telephone seul, aucun, les deuxstring 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 ""+33123456789"".ex:contact3 a ex:Contact ; schema:name ""Charlie"".ex:contact4 a ex:Contact ; schema:name ""Diana""; schema:email ""diana@example.com""; schema:telephone ""+33612345678"".";IGraph contactData =new Graph { BaseUri = UriFactory.Create("http://example.org/")};newTurtleParser().Load(contactData,newStringReader(contactDataTtl));var contactShapeGraph =newShapesGraph(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 AccountShapestring 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, affichezConsole.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 =newGraph();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(newTriple(bookShape,SH("type"),SH("NodeShape")));bookShapes.Assert(newTriple(bookShape,SH("targetClass"),SCHEMA("Book")));// Contrainte 1 : titre obligatoireIBlankNode titlePs = bookShapes.CreateBlankNode();bookShapes.Assert(newTriple(bookShape,SH("property"), titlePs));bookShapes.Assert(newTriple(titlePs,SH("path"),SCHEMA("name")));bookShapes.Assert(newTriple(titlePs,SH("minCount"),(1).ToLiteral(bookShapes)));bookShapes.Assert(newTriple(titlePs,SH("datatype"),XSD("string")));bookShapes.Assert(newTriple(titlePs,SH("minLength"),(1).ToLiteral(bookShapes)));// Contrainte 2 : ISBN avec patternIBlankNode isbnPs = bookShapes.CreateBlankNode();bookShapes.Assert(newTriple(bookShape,SH("property"), isbnPs));bookShapes.Assert(newTriple(isbnPs,SH("path"),SCHEMA("isbn")));bookShapes.Assert(newTriple(isbnPs,SH("datatype"),XSD("string")));bookShapes.Assert(newTriple(isbnPs,SH("pattern"), bookShapes.CreateLiteralNode(@"^97[89]-\d-\d{4}-\d{4}-\d$")));// Contrainte 3 : annee entre 1450 et 2030IBlankNode yearPs = bookShapes.CreateBlankNode();bookShapes.Assert(newTriple(bookShape,SH("property"), yearPs));bookShapes.Assert(newTriple(yearPs,SH("path"),SCHEMA("datePublished")));bookShapes.Assert(newTriple(yearPs,SH("datatype"),XSD("integer")));bookShapes.Assert(newTriple(yearPs,SH("minInclusive"),(1450).ToLiteral(bookShapes)));bookShapes.Assert(newTriple(yearPs,SH("maxInclusive"),(2030).ToLiteral(bookShapes)));// Contrainte 4 : au moins un auteur (IRI de type schema:Person)IBlankNode authorPs = bookShapes.CreateBlankNode();bookShapes.Assert(newTriple(bookShape,SH("property"), authorPs));bookShapes.Assert(newTriple(authorPs,SH("path"),SCHEMA("author")));bookShapes.Assert(newTriple(authorPs,SH("minCount"),(1).ToLiteral(bookShapes)));bookShapes.Assert(newTriple(authorPs,SH("nodeKind"),SH("IRI")));bookShapes.Assert(newTriple(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'auteurex:book3 a schema:Book ; schema:isbn ""123-INVALID"".# Invalide : annee hors bornes(3000>2030), auteur non-Personex: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/")};newTurtleParser().Load(bookData,newStringReader(bookDataTtl));var bookShapeGraph =newShapesGraph(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)
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
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
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.