Partage via


Didacticiel : créer un fournisseur de type

Le mécanisme du fournisseur de type en F# est une partie importante de sa prise en charge de la programmation riche en informations. Ce didacticiel explique comment créer vos propres fournisseurs de types en développant plusieurs fournisseurs de types simples pour illustrer les concepts de base. Pour plus d’informations sur le mécanisme du fournisseur de type dans F#, consultez Fournisseurs de type.

L’écosystème F# contient plusieurs fournisseurs de type intégrés pour les services de données Internet et d'entreprise couramment utilisés. Par exemple :

  • FSharp.Data inclut des fournisseurs de type pour les formats de document JSON, XML, CSV et HTML.

  • SwaggerProvider inclut deux fournisseurs de type génératifs qui génèrent un modèle objet et des clients HTTP pour les API décrites par les schémas OpenApi 3.0 et Swagger 2.0.

  • FSharp.Data.SqlClient a un ensemble de fournisseurs de type pour l’incorporation vérifiée au moment de la compilation de T-SQL dans F#.

Vous pouvez créer des fournisseurs de type personnalisés ou référencer des fournisseurs de type que d’autres utilisateurs ont créés. Par exemple, votre organisation peut disposer d'un service de données fournissant un nombre important et croissant de jeux de données nommées, chacun avec son propre schéma stable de données. Vous pouvez créer un fournisseur de type qui lit les schémas et présente les groupes de données actuels au programmeur de manière fortement typée.

Avant de commencer

Le mécanisme du fournisseur de type est principalement conçu pour injecter des données stables et des espaces d’informations de service dans l’expérience de programmation F#.

Ce mécanisme n’est pas conçu pour injecter des espaces d’informations dont le schéma change pendant l’exécution du programme de façons qui sont pertinentes pour la logique du programme. En outre, le mécanisme n’est pas conçu pour la méta-programmation intra-langage, même si ce domaine contient des utilisations valides. Vous devez utiliser ce mécanisme uniquement lorsqu’il nécessaire et lorsque le développement d’un fournisseur de type génère une valeur très élevée.

Vous devez éviter d’écrire un fournisseur de type dans lequel un schéma n’est pas disponible. De même, vous devez éviter d’écrire un fournisseur de type où une bibliothèque .NET ordinaire (ou même existante) suffirait.

Avant de commencer, vous pouvez poser les questions suivantes :

  • Avez-vous un schéma pour votre source d’informations ? Si c’est le cas, quel est le mappage dans le système de type F# et .NET ?

  • Pouvez-vous utiliser une API existante (typée dynamiquement) comme point de départ pour votre implémentation ?

  • Est-ce que vous et votre organisation aurez suffisamment d’utilisations du fournisseur de type pour que l’écriture en vaut la peine ? Une bibliothèque .NET normale répond-elle à vos besoins ?

  • Dans quelle mesure votre schéma va-t-il changer ?

  • Est-ce que cela changera pendant le codage ?

  • Va-t-il changer entre les sessions de codage ?

  • Est-ce que cela changera pendant l’exécution du programme ?

Les fournisseurs de type sont mieux adaptés aux situations où le schéma est stable au moment de l’exécution et pendant la durée de vie du code compilé.

Un fournisseur de type simple

Cet exemple est Samples.HelloWorldTypeProvider, similaire aux exemples dans le répertoire examples du kit de développement logiciel (SDK) du fournisseur de types F#. Le fournisseur met à disposition un « espace de types » qui contient 100 types effacés, comme le montre le code suivant à l’aide de la syntaxe de signature F# et en omettant les détails pour tous, sauf Type1. Pour plus d’informations sur les types effacés, consultez Détails sur les types fournis effacés plus loin dans cette rubrique.

namespace Samples.HelloWorldTypeProvider

type Type1 =
    /// This is a static property.
    static member StaticProperty : string

    /// This constructor takes no arguments.
    new : unit -> Type1

    /// This constructor takes one argument.
    new : data:string -> Type1

    /// This is an instance property.
    member InstanceProperty : int

    /// This is an instance method.
    member InstanceMethod : x:int -> char

    nested type NestedType =
        /// This is StaticProperty1 on NestedType.
        static member StaticProperty1 : string
        …
        /// This is StaticProperty100 on NestedType.
        static member StaticProperty100 : string

type Type2 =
…
…

type Type100 =
…

Notez que l’ensemble de types et de membres fourni est statiquement connu. Cet exemple ne tire pas profit de la capacité des fournisseurs à fournir des types qui dépendent d’un schéma. L’implémentation du fournisseur de type est décrite dans le code suivant, et les détails sont contenus dans les sections suivantes de cette rubrique.

Avertissement

Il peut y avoir des différences entre ce code et les exemples en ligne.

namespace Samples.FSharp.HelloWorldTypeProvider

open System
open System.Reflection
open ProviderImplementation.ProvidedTypes
open FSharp.Core.CompilerServices
open FSharp.Quotations

// This type defines the type provider. When compiled to a DLL, it can be added
// as a reference to an F# command-line compilation, script, or project.
[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =

  // Inheriting from this type provides implementations of ITypeProvider
  // in terms of the provided types below.
  inherit TypeProviderForNamespaces(config)

  let namespaceName = "Samples.HelloWorldTypeProvider"
  let thisAssembly = Assembly.GetExecutingAssembly()

  // Make one provided type, called TypeN.
  let makeOneProvidedType (n:int) =
  …
  // Now generate 100 types
  let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]

  // And add them to the namespace
  do this.AddNamespace(namespaceName, types)

[<assembly:TypeProviderAssembly>]
do()

Pour utiliser ce fournisseur, ouvrez une instance distincte de Visual Studio, créez un script F#, puis ajoutez une référence au fournisseur à partir de votre script à l’aide de #r comme le montre le code suivant :

#r @".\bin\Debug\Samples.HelloWorldTypeProvider.dll"

let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")

let obj2 = Samples.HelloWorldTypeProvider.Type1("some other data")

obj1.InstanceProperty
obj2.InstanceProperty

[ for index in 0 .. obj1.InstanceProperty-1 -> obj1.InstanceMethod(index) ]
[ for index in 0 .. obj2.InstanceProperty-1 -> obj2.InstanceMethod(index) ]

let data1 = Samples.HelloWorldTypeProvider.Type1.NestedType.StaticProperty35

Recherchez ensuite les types sous l’espace de noms Samples.HelloWorldTypeProvider que le fournisseur de type a générés.

Avant de recompiler le fournisseur, vérifiez que vous avez fermé toutes les instances de Visual Studio et de F# Interactive qui utilisent la DLL du fournisseur. Sinon, une erreur de build se produit, car la DLL de sortie est verrouillée.

Pour déboguer ce fournisseur à l’aide d’instructions print, créez un script qui expose un problème avec le fournisseur, puis utilisez le code suivant :

fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx

Pour déboguer ce fournisseur à l’aide de Visual Studio, ouvrez l’Invite de commandes développeur pour Visual Studio avec des informations d’identification d’administration, puis exécutez la commande suivante :

devenv.exe /debugexe fsc.exe -r:bin\Debug\HelloWorldTypeProvider.dll script.fsx

Vous pouvez également ouvrir Visual Studio, ouvrir le menu Déboguer, choisir Debug/Attach to process…, puis l’attacher à un autre processus devenv dans lequel vous modifiez votre script. En utilisant cette méthode, vous pouvez plus facilement cibler une logique particulière dans le fournisseur de type en tapant de manière interactive des expressions dans la deuxième instance (avec IntelliSense complet et d’autres fonctionnalités).

Vous pouvez désactiver le débogage Uniquement mon code pour mieux identifier les erreurs dans le code généré. Pour plus d’informations sur l’activation ou la désactivation de cette fonctionnalité, consultez Naviguer dans le code avec le débogueur. En outre, vous pouvez également définir l’interception des exceptions de première chance en ouvrant le menu Debug, puis en choisissant Exceptions ou en choisissant les touches Ctrl+Alt+E pour ouvrir la boîte de dialogue Exceptions. Dans cette boîte de dialogue, sous Common Language Runtime Exceptions, activez la case à cocher Thrown.

Implémentation du fournisseur de type

Cette section vous guide tout au long des sections principales de l’implémentation du fournisseur de type. Tout d’abord, vous définissez le type pour le fournisseur de type personnalisé lui-même :

[<TypeProvider>]
type SampleTypeProvider(config: TypeProviderConfig) as this =

Ce type doit être public et vous devez le marquer avec l’attribut TypeProvider afin que le compilateur reconnaisse le fournisseur de type lorsqu’un projet F# distinct fait référence à l’assembly contenant le type. Le paramètre config est facultatif et, le cas échéant, contient des informations de configuration contextuelles pour l’instance de fournisseur de type créée par le compilateur F#.

Ensuite, vous implémentez l’interface ITypeProvider. Dans ce cas, vous utilisez le TypeProviderForNamespaces type de ProvidedTypesl’API comme type de base. Ce type d’assistance peut fournir une collection finie d’espaces de noms fournis immédiatement, chacun d’entre eux contenant directement un nombre fini de types fixes et fournis avec impatience. Dans ce contexte, le fournisseur génère immédiatement des types même s’ils ne sont pas nécessaires ou utilisés.

inherit TypeProviderForNamespaces(config)

Ensuite, définissez des valeurs privées locales qui spécifient l’espace de noms pour les types fournis, puis recherchez l’assembly de fournisseur de type lui-même. Cet assembly est utilisé plus tard comme type parent logique des types effacés fournis.

let namespaceName = "Samples.HelloWorldTypeProvider"
let thisAssembly = Assembly.GetExecutingAssembly()

Ensuite, créez une fonction pour fournir chacun des types :Type1... Type100. Cette fonction est expliquée en détail plus loin dans cette rubrique.

let makeOneProvidedType (n:int) = …

Ensuite, générez les 100 types fournis :

let types = [ for i in 1 .. 100 -> makeOneProvidedType i ]

Puis, ajoutez les types en tant qu’espace de noms fourni :

do this.AddNamespace(namespaceName, types)

Enfin, ajoutez un attribut d’assembly qui indique que vous créez une DLL de fournisseur de types :

[<assembly:TypeProviderAssembly>]
do()

Fournir un type et ses membres

La fonction makeOneProvidedType effectue le travail réel de fournir l’un des types.

let makeOneProvidedType (n:int) =
…

Cette étape explique l’implémentation de cette fonction. Tout d’abord, créez le type fourni (par exemple, Type1, quand n = 1, ou Type57, quand n = 57).

// This is the provided type. It is an erased provided type and, in compiled code,
// will appear as type 'obj'.
let t = ProvidedTypeDefinition(thisAssembly, namespaceName,
                               "Type" + string n,
                               baseType = Some typeof<obj>)

Notez les points suivants :

  • Ce type fourni est effacé. Étant donné que vous indiquez que le type de base est obj, les instances apparaissent comme valeurs de type obj dans le code compilé.

  • Lorsque vous spécifiez un type non imbriqué, vous devez spécifier l’assembly et l’espace de noms. Pour les types effacés, l’assembly doit être l’assembly du fournisseur de type lui-même.

Ensuite, ajoutez la documentation XML au type. Cette documentation est retardée, c’est-à-dire calculée à la demande si le compilateur hôte en a besoin.

t.AddXmlDocDelayed (fun () -> $"""This provided type {"Type" + string n}""")

Ensuite, vous ajoutez une propriété statique fournie au type :

let staticProp = ProvidedProperty(propertyName = "StaticProperty",
                                  propertyType = typeof<string>,
                                  isStatic = true,
                                  getterCode = (fun args -> <@@ "Hello!" @@>))

L’obtention de cette propriété correspond toujours à la chaîne « Hello! ». Le GetterCode pour la propriété utilise une quotation F#, qui représente le code que le compilateur hôte génère pour obtenir la propriété. Pour plus d’informations sur les quotations, consultez Quotations de code (F#).

Ajoutez la documentation XML à la propriété.

staticProp.AddXmlDocDelayed(fun () -> "This is a static property")

Joignez maintenant la propriété fournie au type fourni. Vous devez joindre un membre fourni à un seul type uniquement. Sinon, le membre ne sera jamais accessible.

t.AddMember staticProp

Créez maintenant un constructeur fourni avec aucun paramètre.

let ctor = ProvidedConstructor(parameters = [ ],
                               invokeCode = (fun args -> <@@ "The object data" :> obj @@>))

InvokeCode, pour le constructeur, retourne une quotation F#, qui représente le code que le compilateur hôte génère quand le constructeur est appelé. Par exemple, vous pouvez utiliser le constructeur suivant :

new Type10()

Une instance du type fourni est créée avec les données sous-jacentes « Données de l’objet ». Le code entre guillemets inclut une conversion en obj, car ce type est l’effacement de ce type fourni (comme vous l’avez spécifié lorsque vous avez déclaré le type fourni).

Ajoutez la documentation XML au constructeur et ajoutez le constructeur fourni au type fourni :

ctor.AddXmlDocDelayed(fun () -> "This is a constructor")

t.AddMember ctor

Créez un deuxième constructeur fourni qui prend un paramètre :

let ctor2 =
ProvidedConstructor(parameters = [ ProvidedParameter("data",typeof<string>) ],
                    invokeCode = (fun args -> <@@ (%%(args[0]) : string) :> obj @@>))

InvokeCode, pour le constructeur, retourne encore une quotation F#, qui représente le code que le compilateur hôte génère quand le pour un appel à la méthode. Par exemple, vous pouvez utiliser le constructeur suivant :

new Type10("ten")

Une instance du type fourni est créée avec les données sous-jacentes « ten ». Vous avez peut-être déjà remarqué que la fonction InvokeCode retourne une quotation. L’entrée de cette fonction est une liste d’expressions, une par paramètre de constructeur. Dans ce cas, une expression qui représente la valeur du paramètre unique est disponible dans args[0]. Le code d’un appel au constructeur force la valeur renvoyée au type objeffacé. Après avoir ajouté le deuxième constructeur fourni au type, créez une propriété d’instance fourni :

let instanceProp =
    ProvidedProperty(propertyName = "InstanceProperty",
                     propertyType = typeof<int>,
                     getterCode= (fun args ->
                        <@@ ((%%(args[0]) : obj) :?> string).Length @@>))
instanceProp.AddXmlDocDelayed(fun () -> "This is an instance property")
t.AddMember instanceProp

L’obtention de cette propriété renvoie la longueur de la chaîne, qui est l’objet de représentation. La propriété GetterCode retourne une quotation F# qui spécifie le code que le compilateur hôte génère pour obtenir la propriété. Comme InvokeCode, la fonction GetterCode retourne une quotation. Le compilateur hôte appelle cette fonction avec une liste d’arguments. Dans ce cas, les arguments incluent uniquement l’expression unique qui représente l’instance sur laquelle le getter est appelé, et auquel vous pouvez accéder à l’aide de args[0]. L’implémentation de GetterCode se joint ensuite à la quotation de résultat au niveau du type objeffacé et un cast est utilisé pour satisfaire le mécanisme du compilateur pour vérifier les types dont l’objet est une chaîne. La partie suivante de makeOneProvidedType fournit une méthode d’instance avec un paramètre.

let instanceMeth =
    ProvidedMethod(methodName = "InstanceMethod",
                   parameters = [ProvidedParameter("x",typeof<int>)],
                   returnType = typeof<char>,
                   invokeCode = (fun args ->
                       <@@ ((%%(args[0]) : obj) :?> string).Chars(%%(args[1]) : int) @@>))

instanceMeth.AddXmlDocDelayed(fun () -> "This is an instance method")
// Add the instance method to the type.
t.AddMember instanceMeth

Enfin, créez un type imbriqué qui contient 100 propriétés imbriquées. La création de ce type imbriqué et de ses propriétés est retardée, c’est-à-dire calculée à la demande.

t.AddMembersDelayed(fun () ->
  let nestedType = ProvidedTypeDefinition("NestedType", Some typeof<obj>)

  nestedType.AddMembersDelayed (fun () ->
    let staticPropsInNestedType =
      [
          for i in 1 .. 100 ->
              let valueOfTheProperty = "I am string "  + string i

              let p =
                ProvidedProperty(propertyName = "StaticProperty" + string i,
                  propertyType = typeof<string>,
                  isStatic = true,
                  getterCode= (fun args -> <@@ valueOfTheProperty @@>))

              p.AddXmlDocDelayed(fun () ->
                  $"This is StaticProperty{i} on NestedType")

              p
      ]

    staticPropsInNestedType)

  [nestedType])

Détails sur les types fournis effacés

L’exemple de cette section fournit uniquement les types fournis effacés, qui sont particulièrement utiles dans les situations suivantes :

  • Lorsque vous écrivez un fournisseur pour un espace d’informations qui contient uniquement des données et des méthodes.

  • Lorsque vous écrivez un fournisseur où une sémantique de type runtime précise n’est pas critique pour une utilisation pratique de l’espace d’informations.

  • Lorsque vous écrivez un fournisseur pour un espace d’informations si volumineux et interconnecté qu’il n’est techniquement pas possible de générer des types .NET réels pour l’espace d’informations.

Dans cet exemple, chaque type fourni est effacé en type obj et toutes les utilisations du type apparaissent en tant que type obj dans le code compilé. En fait, les objets sous-jacents dans ces exemples sont des chaînes, mais le type apparaît comme System.Object dans le code compilé .NET. Comme pour toutes les utilisations de l’effacement de type, vous pouvez utiliser le boxing, l’unboxing et le cast explicites pour subvertir les types effacés. Dans ce cas, une exception cast qui n’est pas valide peut se produire lorsque l’objet est utilisé. Un runtime de fournisseur peut définir son propre type de représentation privée pour se protéger contre les fausses représentations. Vous ne pouvez pas définir de types effacés dans F# lui-même. Seuls les types fournis peuvent être effacés. Vous devez comprendre les implications, à la fois pratiques et sémantiques, de l’utilisation de types effacés pour votre fournisseur de types ou pour un fournisseur qui fournit des types effacés. Un type effacé n’a pas de type .NET réel. Par conséquent, vous ne pouvez pas effectuer une réflexion précise sur le type, et vous pouvez subvertir les types effacés si vous utilisez des casts runtime et d’autres techniques qui s’appuient sur la sémantique exacte du type d’exécution. La subversion des types effacés entraîne fréquemment des exceptions de type cast au moment de l’exécution.

Choix des représentations pour les types fournis effacés

Pour certaines utilisations des types fournis effacés, aucune représentation n’est requise. Par exemple, le type fourni effacé peut contenir uniquement des propriétés et des membres statiques et aucun constructeur, et aucune méthode ou propriété ne retournerait une instance du type. Si vous pouvez atteindre des instances d’un type fourni effacé, vous devez prendre en compte les questions suivantes :

Qu’est-ce que l’effacement d’un type fourni ?

  • L’effacement d’un type fourni est la manière dont le type apparaît dans le code .NET compilé.

  • L’effacement d’un type de classe effacé fourni est toujours le premier type de base non effacé dans la chaîne d’héritage du type.

  • L’effacement d’un type d’interface effacé fourni est toujours System.Object.

Quelles sont les représentations d’un type fourni ?

  • L’ensemble des objets possibles pour un type fourni effacé est appelé ses représentations. Dans l’exemple de ce document, les représentations de tous les types fournis effacés Type1..Type100 sont toujours des objets de chaîne.

Toutes les représentations d’un type fourni doivent être compatibles avec l’effacement du type fourni. (Sinon, soit le compilateur F# génère une erreur pour une utilisation du fournisseur de type, soit du code .NET non vérifiable et non valide est généré. Un fournisseur de type n’est pas valide s’il retourne du code qui donne une représentation qui n’est pas valide.)

Vous pouvez choisir une représentation pour les objets fournis en utilisant l’une des approches suivantes, qui sont toutes deux très courantes :

  • Si vous fournissez simplement un wrapper fortement typé sur un type .NET existant, il est souvent plus judicieux pour votre type d’effacer ce type, d’utiliser des instances de ce type en tant que représentations, ou les deux. Cette approche est appropriée lorsque la plupart des méthodes existantes de ce type ont toujours un sens lors de l’utilisation de la version fortement typée.

  • Si vous souhaitez créer une API qui diffère considérablement de n’importe quelle API .NET existante, il est judicieux de créer des types de runtime qui seront l’effacement du type et les représentations pour les types fournis.

L’exemple dans ce document utilise des chaînes comme représentations d’objets fournis. Souvent, il peut être approprié d’utiliser d’autres objets pour les représentations. Par exemple, vous pouvez utiliser un dictionnaire comme sac de propriétés :

ProvidedConstructor(parameters = [],
    invokeCode= (fun args -> <@@ (new Dictionary<string,obj>()) :> obj @@>))

Vous pouvez également définir un type dans votre fournisseur de type qui sera utilisé au moment de l’exécution pour former la représentation, ainsi qu’une ou plusieurs opérations d’exécution :

type DataObject() =
    let data = Dictionary<string,obj>()
    member x.RuntimeOperation() = data.Count

Les membres fournis peuvent ensuite construire des instances de ce type d’objet :

ProvidedConstructor(parameters = [],
    invokeCode= (fun args -> <@@ (new DataObject()) :> obj @@>))

Dans ce cas, vous pouvez (éventuellement) utiliser ce type comme effacement de type en le spécifiant comme baseType lors de la construction de ProvidedTypeDefinition :

ProvidedTypeDefinition(…, baseType = Some typeof<DataObject> )
…
ProvidedConstructor(…, InvokeCode = (fun args -> <@@ new DataObject() @@>), …)

Principaux enseignements

La section précédente a expliqué comment créer un fournisseur de type d’effacement simple qui fournit une plage de types, de propriétés et de méthodes. Cette section a également expliqué le concept d’effacement de type, y compris certains des avantages et des inconvénients de la fourniture de types effacés à partir d’un fournisseur de type et a abordé les représentations des types effacés.

Fournisseur de type qui utilise des paramètres statiques

La possibilité de paramétrer des fournisseurs de type par des données statiques permet de nombreux scénarios intéressants, même dans les cas où le fournisseur n’a pas besoin d’accéder à des données locales ou distantes. Dans cette section, vous allez découvrir certaines techniques de base pour mettre en place un tel fournisseur.

Fournisseur de type Regex vérifié

Imaginez que vous souhaitiez implémenter un fournisseur de type pour les expressions régulières qui incluent dans un wrapper des bibliothèques Regex .NET dans une interface qui fournit les garanties de compilation suivantes :

  • Vérification de la validité d’une expression régulière.

  • Fourniture de propriétés nommées sur des correspondances basées sur des noms de groupe dans l’expression régulière.

Cette section vous montre comment utiliser des fournisseurs de type pour créer un type RegexTypedque le modèle d’expression régulière paramètre pour fournir ces avantages. Le compilateur signale une erreur si le modèle fourni n’est pas valide, et le fournisseur de type peut extraire les groupes du modèle afin que vous puissiez y accéder à l’aide de propriétés nommées sur les correspondances. Lorsque vous concevez un fournisseur de type, vous devez prendre en compte l’apparence de son API exposée aux utilisateurs finaux et la façon dont cette conception se traduit en code .NET. L’exemple suivant montre comment utiliser une telle API pour obtenir les composants de l’indicatif régional :

type T = RegexTyped< @"(?<AreaCode>^\d{3})-(?<PhoneNumber>\d{3}-\d{4}$)">
let reg = T()
let result = T.IsMatch("425-555-2345")
let r = reg.Match("425-555-2345").Group_AreaCode.Value //r equals "425"

L’exemple suivant montre comment le fournisseur de type traduit ces appels :

let reg = new Regex(@"(?<AreaCode>^\d{3})-(?<PhoneNumber>\d{3}-\d{4}$)")
let result = reg.IsMatch("425-123-2345")
let r = reg.Match("425-123-2345").Groups["AreaCode"].Value //r equals "425"

Notez les points suivants :

  • Le type Regex standard représente le type paramétrable RegexTyped.

  • Le constructeur RegexTyped entraîne un appel au constructeur Regex, en passant l’argument de type statique pour le modèle.

  • Les résultats de la méthode Match sont représentés par le type standard Match.

  • Chaque groupe nommé génère une propriété fournie et l’accès à la propriété entraîne l’utilisation d’un indexeur sur la collection d’une correspondance Groups.

Le code suivant est le cœur de la logique d’implémentation d’un tel fournisseur et cet exemple omet l’ajout de tous les membres au type fourni. Pour plus d’informations sur chaque membre ajouté, consultez la section appropriée plus loin dans cette rubrique.

namespace Samples.FSharp.RegexTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.Text.RegularExpressions

[<TypeProvider>]
type public CheckedRegexProvider() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.FSharp.RegexTypeProvider"
    let baseTy = typeof<obj>
    let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]

    let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "RegexTyped", Some baseTy)

    do regexTy.DefineStaticParameters(
        parameters=staticParams,
        instantiationFunction=(fun typeName parameterValues ->

          match parameterValues with
          | [| :? string as pattern|] ->

            // Create an instance of the regular expression.
            //
            // This will fail with System.ArgumentException if the regular expression is not valid.
            // The exception will escape the type provider and be reported in client code.
            let r = System.Text.RegularExpressions.Regex(pattern)

            // Declare the typed regex provided type.
            // The type erasure of this type is 'obj', even though the representation will always be a Regex
            // This, combined with hiding the object methods, makes the IntelliSense experience simpler.
            let ty =
              ProvidedTypeDefinition(
                thisAssembly,
                rootNamespace,
                typeName,
                baseType = Some baseTy)

            ...

            ty
          | _ -> failwith "unexpected parameter values"))

    do this.AddNamespace(rootNamespace, [regexTy])

[<TypeProviderAssembly>]
do ()

Notez les points suivants :

  • Le fournisseur de type prend deux paramètres statiques : le pattern, qui est obligatoire, et les options, qui sont facultatives (car une valeur par défaut est fournie).

  • Une fois les arguments statiques fournis, créez une instance de l’expression régulière. Cette instance lève une exception si l’expression regex est malformée et cette erreur est signalée aux utilisateurs.

  • Dans le rappel DefineStaticParameters, définissez le type qui sera retourné une fois les arguments fournis.

  • Ce code définit la valeur HideObjectMethods sur true afin que l’expérience IntelliSense reste rationalisée. Cet attribut entraîne la suppression des membres Equals, GetHashCode, Finalize et GetType des listes IntelliSense pour un objet fourni.

  • Vous utilisez obj comme type de base de la méthode, mais vous allez utiliser un objet Regex comme représentation runtime de ce type, comme le montre l’exemple suivant.

  • L’appel au constructeur Regex lève un ArgumentException quand une expression régulière n’est pas valide. Le compilateur intercepte cette exception et envoie un message d’erreur à l’utilisateur au moment de la compilation ou dans l’éditeur Visual Studio. Cette exception permet aux expressions régulières d’être validées sans exécuter d’application.

Le type défini ci-dessus n’est pas encore utile, car il ne contient pas de méthodes ni de propriétés significatives. Tout d’abord, ajoutez une méthode statique IsMatch :

let isMatch =
    ProvidedMethod(
        methodName = "IsMatch",
        parameters = [ProvidedParameter("input", typeof<string>)],
        returnType = typeof<bool>,
        isStatic = true,
        invokeCode = fun args -> <@@ Regex.IsMatch(%%args[0], pattern) @@>)

isMatch.AddXmlDoc "Indicates whether the regular expression finds a match in the specified input string."
ty.AddMember isMatch

Le code précédent définit une méthode IsMatch, qui prend une chaîne comme entrée et retourne un bool. Le seul problème est l’utilisation de l’argument args dans la définition InvokeCode. Dans cet exemple, args est une liste de quotations qui représentent les arguments de cette méthode. Si la méthode est une méthode d’instance, le premier argument représente l’argument this. Toutefois, pour une méthode statique, les arguments ne sont que les arguments explicites de la méthode. Notez que le type de la valeur entre guillemets doit correspondre au type de retour spécifié (dans ce cas, bool). Notez également que ce code utilise la méthode AddXmlDoc pour vous assurer que la méthode fournie dispose également d’une documentation utile, que vous pouvez fournir via IntelliSense.

Ensuite, ajoutez une méthode Match d’instance. Cependant, cette méthode doit retourner une valeur d’un type Match fourni afin que les groupes soient accessibles de manière fortement typée. Par conséquent, déclarez d’abord le type Match. Étant donné que ce type dépend du modèle fourni en tant qu’argument statique, il doit être imbriqué dans la définition de type paramétrable :

let matchTy =
    ProvidedTypeDefinition(
        "MatchType",
        baseType = Some baseTy,
        hideObjectMethods = true)

ty.AddMember matchTy

Ajoutez ensuite une propriété au type Match pour chaque groupe. Au moment de l’exécution, une correspondance est représentée sous la forme d’une valeur Match. Par conséquent, la quotation qui définit la propriété doit utiliser la propriété indexée Groups pour obtenir le groupe approprié.

for group in r.GetGroupNames() do
    // Ignore the group named 0, which represents all input.
    if group <> "0" then
    let prop =
      ProvidedProperty(
        propertyName = group,
        propertyType = typeof<Group>,
        getterCode = fun args -> <@@ ((%%args[0]:obj) :?> Match).Groups[group] @@>)
        prop.AddXmlDoc($"""Gets the ""{group}"" group from this match""")
    matchTy.AddMember prop

De nouveau, notez que vous ajoutez la documentation XML à la propriété fournie. Notez également qu’une propriété peut être lue si une fonction GetterCode est fournie et que la propriété peut être écrite si une fonction SetterCode est fournie, de sorte que la propriété résultante est en lecture seule.

Vous pouvez maintenant créer une méthode d’instance qui retourne une valeur de ce type Match :

let matchMethod =
    ProvidedMethod(
        methodName = "Match",
        parameters = [ProvidedParameter("input", typeof<string>)],
        returnType = matchTy,
        invokeCode = fun args -> <@@ ((%%args[0]:obj) :?> Regex).Match(%%args[1]) :> obj @@>)

matchMeth.AddXmlDoc "Searches the specified input string for the first occurrence of this regular expression"

ty.AddMember matchMeth

Étant donné que vous créez une méthode d’instance, args[0] représente l’instance RegexTyped sur laquelle la méthode est appelée et args[1] est l’argument d’entrée.

Enfin, fournissez un constructeur pour que des instances du type fourni puissent être créées.

let ctor =
    ProvidedConstructor(
        parameters = [],
        invokeCode = fun args -> <@@ Regex(pattern, options) :> obj @@>)

ctor.AddXmlDoc("Initializes a regular expression instance.")

ty.AddMember ctor

Le constructeur efface simplement la création d’une instance Regex .NET standard, qui est à nouveau boxée dans un objet, car obj est l’effacement du type fourni. Avec cette modification, l’exemple d’utilisation de l’API spécifié précédemment dans cette rubrique fonctionne comme prévu. Le code suivant est complet et final :

namespace Samples.FSharp.RegexTypeProvider

open System.Reflection
open Microsoft.FSharp.Core.CompilerServices
open Samples.FSharp.ProvidedTypes
open System.Text.RegularExpressions

[<TypeProvider>]
type public CheckedRegexProvider() as this =
    inherit TypeProviderForNamespaces()

    // Get the assembly and namespace used to house the provided types.
    let thisAssembly = Assembly.GetExecutingAssembly()
    let rootNamespace = "Samples.FSharp.RegexTypeProvider"
    let baseTy = typeof<obj>
    let staticParams = [ProvidedStaticParameter("pattern", typeof<string>)]

    let regexTy = ProvidedTypeDefinition(thisAssembly, rootNamespace, "RegexTyped", Some baseTy)

    do regexTy.DefineStaticParameters(
        parameters=staticParams,
        instantiationFunction=(fun typeName parameterValues ->

            match parameterValues with
            | [| :? string as pattern|] ->

                // Create an instance of the regular expression.

                let r = System.Text.RegularExpressions.Regex(pattern)

                // Declare the typed regex provided type.

                let ty =
                    ProvidedTypeDefinition(
                        thisAssembly,
                        rootNamespace,
                        typeName,
                        baseType = Some baseTy)

                ty.AddXmlDoc "A strongly typed interface to the regular expression '%s'"

                // Provide strongly typed version of Regex.IsMatch static method.
                let isMatch =
                    ProvidedMethod(
                        methodName = "IsMatch",
                        parameters = [ProvidedParameter("input", typeof<string>)],
                        returnType = typeof<bool>,
                        isStatic = true,
                        invokeCode = fun args -> <@@ Regex.IsMatch(%%args[0], pattern) @@>)

                isMatch.AddXmlDoc "Indicates whether the regular expression finds a match in the specified input string"

                ty.AddMember isMatch

                // Provided type for matches
                // Again, erase to obj even though the representation will always be a Match
                let matchTy =
                    ProvidedTypeDefinition(
                        "MatchType",
                        baseType = Some baseTy,
                        hideObjectMethods = true)

                // Nest the match type within parameterized Regex type.
                ty.AddMember matchTy

                // Add group properties to match type
                for group in r.GetGroupNames() do
                    // Ignore the group named 0, which represents all input.
                    if group <> "0" then
                        let prop =
                          ProvidedProperty(
                            propertyName = group,
                            propertyType = typeof<Group>,
                            getterCode = fun args -> <@@ ((%%args[0]:obj) :?> Match).Groups[group] @@>)
                        prop.AddXmlDoc(sprintf @"Gets the ""%s"" group from this match" group)
                        matchTy.AddMember(prop)

                // Provide strongly typed version of Regex.Match instance method.
                let matchMeth =
                  ProvidedMethod(
                    methodName = "Match",
                    parameters = [ProvidedParameter("input", typeof<string>)],
                    returnType = matchTy,
                    invokeCode = fun args -> <@@ ((%%args[0]:obj) :?> Regex).Match(%%args[1]) :> obj @@>)
                matchMeth.AddXmlDoc "Searches the specified input string for the first occurrence of this regular expression"

                ty.AddMember matchMeth

                // Declare a constructor.
                let ctor =
                  ProvidedConstructor(
                    parameters = [],
                    invokeCode = fun args -> <@@ Regex(pattern) :> obj @@>)

                // Add documentation to the constructor.
                ctor.AddXmlDoc "Initializes a regular expression instance"

                ty.AddMember ctor

                ty
            | _ -> failwith "unexpected parameter values"))

    do this.AddNamespace(rootNamespace, [regexTy])

[<TypeProviderAssembly>]
do ()

Principaux enseignements

Cette section explique comment créer un fournisseur de type qui fonctionne sur ses paramètres statiques. Le fournisseur vérifie le paramètre statique et fournit des opérations en fonction de sa valeur.

Fournisseur de type qui est soutenu par des données locales

Il se peut que vous souhaitiez souvent que les fournisseurs de type présentent des API basées non seulement sur des paramètres statiques, mais également sur des informations provenant de systèmes locaux ou distants. Cette section décrit les fournisseurs de type basés sur des données locales, telles que des fichiers de données locales.

Fournisseur de fichiers CSV simple

Prenons l’exemple d’un fournisseur de type pour accéder aux données scientifiques au format CSV (Comma Separated Value). Cette section suppose que les fichiers CSV contiennent une ligne d’en-tête suivie de données à virgule flottante, comme l’illustre le tableau suivant :

Distance (mètre) Heure (seconde)
50,0 3.7
100.0 5.2
150.0 6.4

Cette section montre comment fournir un type que vous pouvez utiliser pour obtenir des lignes avec une propriété Distance de type float<meter> et une propriété Time de type float<second>. Par souci de simplicité, les hypothèses suivantes sont faites :

  • Les noms d’en-tête sont soit sans unité soit ont la forme « Nom (unité) » et ne contiennent pas de virgules.

  • Les unités sont toutes des unités du système international telles que le définit le module FSharp.Data.UnitSystems.SI.UnitNames Module (F#).

  • Les unités sont toutes simples (par exemple, mètre) plutôt que composées (par exemple, mètre/seconde).

  • Toutes les colonnes contiennent des données à virgule flottante.

Un fournisseur plus complet assouplirait ces restrictions.

De nouveau, la première étape consiste à examiner l’apparence de l’API. Étant donné un fichier info.csv contenant le contenu du tableau précédente (au format séparé par des virgules), les utilisateurs du fournisseur doivent pouvoir écrire du code semblable à l’exemple suivant :

let info = new MiniCsv<"info.csv">()
for row in info.Data do
let time = row.Time
printfn $"{float time}"

Dans ce cas, le compilateur doit convertir ces appels en un code qui ressemble à l’exemple suivant :

let info = new CsvFile("info.csv")
for row in info.Data do
let (time:float) = row[1]
printfn $"%f{float time}"

La traduction optimale nécessite que le fournisseur de type définisse un type CsvFile réel dans l’assembly du fournisseur de type. Les fournisseurs de types s’appuient souvent sur quelques types et méthodes d’assistance pour inclure dans un wrapper une logique importante. Étant donné que les mesures sont effacées au moment de l’exécution, vous pouvez utiliser un float[] comme type effacé pour une ligne. Le compilateur traitera les différentes colonnes comme ayant différents types de mesures. Par exemple, la première colonne de notre exemple a le type float<meter>, et la deuxième a float<second>. Toutefois, la représentation effacée peut rester assez simple.

Le code suivant montre le cœur de l’implémentation.

// Simple type wrapping CSV data
type CsvFile(filename) =
    // Cache the sequence of all data lines (all lines but the first)
    let data =
        seq {
            for line in File.ReadAllLines(filename) |> Seq.skip 1 ->
                line.Split(',') |> Array.map float
        }
        |> Seq.cache
    member _.Data = data

[<TypeProvider>]
type public MiniCsvProvider(cfg:TypeProviderConfig) as this =
    inherit TypeProviderForNamespaces(cfg)

    // Get the assembly and namespace used to house the provided types.
    let asm = System.Reflection.Assembly.GetExecutingAssembly()
    let ns = "Samples.FSharp.MiniCsvProvider"

    // Create the main provided type.
    let csvTy = ProvidedTypeDefinition(asm, ns, "MiniCsv", Some(typeof<obj>))

    // Parameterize the type by the file to use as a template.
    let filename = ProvidedStaticParameter("filename", typeof<string>)
    do csvTy.DefineStaticParameters([filename], fun tyName [| :? string as filename |] ->

        // Resolve the filename relative to the resolution folder.
        let resolvedFilename = Path.Combine(cfg.ResolutionFolder, filename)

        // Get the first line from the file.
        let headerLine = File.ReadLines(resolvedFilename) |> Seq.head

        // Define a provided type for each row, erasing to a float[].
        let rowTy = ProvidedTypeDefinition("Row", Some(typeof<float[]>))

        // Extract header names from the file, splitting on commas.
        // use Regex matching to get the position in the row at which the field occurs
        let headers = Regex.Matches(headerLine, "[^,]+")

        // Add one property per CSV field.
        for i in 0 .. headers.Count - 1 do
            let headerText = headers[i].Value

            // Try to decompose this header into a name and unit.
            let fieldName, fieldTy =
                let m = Regex.Match(headerText, @"(?<field>.+) \((?<unit>.+)\)")
                if m.Success then

                    let unitName = m.Groups["unit"].Value
                    let units = ProvidedMeasureBuilder.Default.SI unitName
                    m.Groups["field"].Value, ProvidedMeasureBuilder.Default.AnnotateType(typeof<float>,[units])

                else
                    // no units, just treat it as a normal float
                    headerText, typeof<float>

            let prop =
                ProvidedProperty(fieldName, fieldTy,
                    getterCode = fun [row] -> <@@ (%%row:float[])[i] @@>)

            // Add metadata that defines the property's location in the referenced file.
            prop.AddDefinitionLocation(1, headers[i].Index + 1, filename)
            rowTy.AddMember(prop)

        // Define the provided type, erasing to CsvFile.
        let ty = ProvidedTypeDefinition(asm, ns, tyName, Some(typeof<CsvFile>))

        // Add a parameterless constructor that loads the file that was used to define the schema.
        let ctor0 =
            ProvidedConstructor([],
                invokeCode = fun [] -> <@@ CsvFile(resolvedFilename) @@>)
        ty.AddMember ctor0

        // Add a constructor that takes the file name to load.
        let ctor1 = ProvidedConstructor([ProvidedParameter("filename", typeof<string>)],
            invokeCode = fun [filename] -> <@@ CsvFile(%%filename) @@>)
        ty.AddMember ctor1

        // Add a more strongly typed Data property, which uses the existing property at run time.
        let prop =
            ProvidedProperty("Data", typedefof<seq<_>>.MakeGenericType(rowTy),
                getterCode = fun [csvFile] -> <@@ (%%csvFile:CsvFile).Data @@>)
        ty.AddMember prop

        // Add the row type as a nested type.
        ty.AddMember rowTy
        ty)

    // Add the type to the namespace.
    do this.AddNamespace(ns, [csvTy])

Notez les points suivants à propos de l’implémentation :

  • Les constructeurs surchargés autorisent la lecture du fichier d’origine ou d’un fichier ayant un schéma identique. Ce modèle est courant lorsque vous écrivez un fournisseur de type pour des sources de données locales ou distantes et ce modèle permet d’utiliser un fichier local comme modèle pour les données distantes.

  • Vous pouvez utiliser la valeur TypeProviderConfig transmise au constructeur du fournisseur de type pour résoudre les noms de fichiers relatifs.

  • Vous pouvez utiliser la méthode AddDefinitionLocation pour définir l’emplacement des propriétés fournies. Par conséquent, si vous utilisez Go To Definition sur une propriété fournie, le fichier CSV s’ouvre dans Visual Studio.

  • Vous pouvez utiliser le type ProvidedMeasureBuilder pour rechercher les unités du système international et générer les types float<_> appropriés.

Principaux enseignements

Cette section explique comment créer un fournisseur de type pour une source de données locale avec un schéma simple contenu dans la source de données elle-même.

Aller plus loin

Les sections suivantes contiennent des suggestions pour une étude plus approfondie.

Aperçu du code compilé pour les types effacés

Pour avoir une idée de la façon dont l’utilisation du fournisseur de type correspond au code émis, examinez la fonction suivante en utilisant le HelloWorldTypeProvider qui est utilisé plus haut dans cette rubrique.

let function1 () =
    let obj1 = Samples.HelloWorldTypeProvider.Type1("some data")
    obj1.InstanceProperty

Voici une image du code résultant décompilé à l’aide de ildasm.exe :

.class public abstract auto ansi sealed Module1
extends [mscorlib]System.Object
{
.custom instance void [FSharp.Core]Microsoft.FSharp.Core.CompilationMappingAtt
ribute::.ctor(valuetype [FSharp.Core]Microsoft.FSharp.Core.SourceConstructFlags)
= ( 01 00 07 00 00 00 00 00 )
.method public static int32  function1() cil managed
{
// Code size       24 (0x18)
.maxstack  3
.locals init ([0] object obj1)
IL_0000:  nop
IL_0001:  ldstr      "some data"
IL_0006:  unbox.any  [mscorlib]System.Object
IL_000b:  stloc.0
IL_000c:  ldloc.0
IL_000d:  call       !!0 [FSharp.Core_2]Microsoft.FSharp.Core.LanguagePrimit
ives/IntrinsicFunctions::UnboxGeneric<string>(object)
IL_0012:  callvirt   instance int32 [mscorlib_3]System.String::get_Length()
IL_0017:  ret
} // end of method Module1::function1

} // end of class Module1

Comme le montre l’exemple, toutes les mentions du type Type1 et de la propriété InstanceProperty ont été effacées, laissant uniquement les opérations sur les types runtime impliqués.

Conception et conventions d’affectation de noms pour les fournisseurs de type

Observez les conventions suivantes lors de la création de fournisseurs de type.

Fournisseurs pour les protocoles de connectivité En général, les noms de la plupart des DLL de fournisseur pour les protocoles de connectivité de données et de service, tels que les connexions OData ou SQL, doivent se terminer par TypeProvider ou TypeProviders. Par exemple, utilisez un nom de DLL qui ressemble à la chaîne suivante :

Fabrikam.Management.BasicTypeProviders.dll

Assurez-vous que vos types fournis sont membres de l’espace de noms correspondant et indiquez le protocole de connectivité que vous avez implémenté :

  Fabrikam.Management.BasicTypeProviders.WmiConnection<…>
  Fabrikam.Management.BasicTypeProviders.DataProtocolConnection<…>

Fournisseurs d’utilitaires pour le codage général. Pour un fournisseur de type utilitaire comme celui pour les expressions régulières, le fournisseur de type peut faire partie d’une bibliothèque de base, comme le montre l’exemple suivant :

#r "Fabrikam.Core.Text.Utilities.dll"

Dans ce cas, le type fourni apparaît à un point approprié conformément aux conventions de conception .NET normales :

  open Fabrikam.Core.Text.RegexTyped

  let regex = new RegexTyped<"a+b+a+b+">()

Sources de données Singleton. Certains fournisseurs de type se connectent à une seule source de données dédiée et fournissent uniquement des données. Dans ce cas, vous devez supprimer le suffixe TypeProvider et utiliser les conventions normales d’affectation de noms .NET :

#r "Fabrikam.Data.Freebase.dll"

let data = Fabrikam.Data.Freebase.Astronomy.Asteroids

Pour plus d’informations, consultez la convention de conception GetConnection décrite plus loin dans cette rubrique.

Modèles de conception pour les fournisseurs de type

Les sections suivantes décrivent les modèles de conception que vous pouvez utiliser lors de la création de fournisseurs de type.

Modèle de conception GetConnection

La plupart des fournisseurs de type doivent être écrits pour utiliser le modèle GetConnection utilisé par les fournisseurs de type dans FSharp.Data.TypeProviders.dll, comme le montre l’exemple suivant :

#r "Fabrikam.Data.WebDataStore.dll"

type Service = Fabrikam.Data.WebDataStore<…static connection parameters…>

let connection = Service.GetConnection(…dynamic connection parameters…)

let data = connection.Astronomy.Asteroids

Fournisseurs de type soutenus par des données et services distants

Avant de créer un fournisseur de type soutenu par des données et des services distants, vous devez tenir compte d’une série de problèmes inhérents à la programmation connectée. Ces problèmes incluent les éléments à prendre en considération suivants :

  • mappage de schéma

  • liveness et invalidation en présence d’une modification de schéma

  • mise en cache des schémas

  • implémentations asynchrones d’opérations d’accès aux données

  • prise en charge des requêtes, y compris les requêtes LINQ

  • informations d’identification et authentification

Cette rubrique n’analyse pas davantage ces problèmes.

Techniques de création supplémentaires

Lorsque vous écrivez vos propres fournisseurs de type, vous pouvez utiliser les techniques supplémentaires suivantes.

Création de types et de membres à la demande

L’API ProvidedType a des versions retardées d’AddMember.

  type ProvidedType =
      member AddMemberDelayed  : (unit -> MemberInfo)      -> unit
      member AddMembersDelayed : (unit -> MemberInfo list) -> unit

Ces versions sont utilisées pour créer des espaces à la demande de types.

Fourniture de types de tableaux et d’instanciations de type générique

Vous créez des membres fournis (dont les signatures incluent des types de tableau, des types byref et des instanciations de types génériques) en utilisant la valeur normale MakeArrayType, MakePointerTypeet MakeGenericType sur n’importe quelle instance de Type, y compris ProvidedTypeDefinitions.

Notes

Dans certains cas, vous devrez peut-être utiliser l’assistance dans ProvidedTypeBuilder.MakeGenericType. Pour plus d’informations, consultez la documentation du kit de développement logiciel (SDK) du fournisseur de type.

Fourniture d’annotations d’unité de mesure

L’API ProvidedTypes offre des assistances pour fournir des annotations de mesure. Par exemple, pour fournir le type float<kg>, utilisez le code suivant :

  let measures = ProvidedMeasureBuilder.Default
  let kg = measures.SI "kilogram"
  let m = measures.SI "meter"
  let float_kg = measures.AnnotateType(typeof<float>,[kg])

Pour fournir le type Nullable<decimal<kg/m^2>>, utilisez le code suivant :

  let kgpm2 = measures.Ratio(kg, measures.Square m)
  let dkgpm2 = measures.AnnotateType(typeof<decimal>,[kgpm2])
  let nullableDecimal_kgpm2 = typedefof<System.Nullable<_>>.MakeGenericType [|dkgpm2 |]

Accès aux ressources Project-Local ou Script-Local

Chaque instance d’un fournisseur de type peut recevoir une valeur TypeProviderConfig pendant la construction. Cette valeur contient le « dossier de résolution » pour le fournisseur (c’est-à-dire le dossier de projet pour la compilation ou le répertoire qui contient un script), la liste des assemblys référencés et d’autres informations.

Invalidation

Les fournisseurs peuvent déclencher des signaux d’invalidation pour informer le service de langage F# que les hypothèses de schéma ont peut-être changé. Lors d’une invalidation, une vérification de type est refaite si le fournisseur est hébergé dans Visual Studio. Ce signal est ignoré lorsque le fournisseur est hébergé dans F# Interactive ou par le compilateur F# (fsc.exe).

Mise en cache des informations de schéma

Les fournisseurs doivent souvent mettre en cache l’accès aux informations de schéma. Les données mises en cache doivent être stockées à l’aide d’un nom de fichier donné en tant que paramètre statique ou données utilisateur. Un exemple de mise en cache de schéma est le paramètre LocalSchemaFile dans les fournisseurs de type dans l’assembly FSharp.Data.TypeProviders. Lors de l’implémentation de ces fournisseurs, ce paramètre statique indique au fournisseur de type d’utiliser les informations de schéma dans le fichier local spécifié au lieu d’accéder à la source de données via le réseau. Pour utiliser les informations de schéma mises en cache, vous devez également définir le paramètre statique ForceUpdate sur false. Vous pouvez utiliser une technique similaire pour activer l’accès aux données en ligne et hors connexion.

Assembly de stockage

Lorsque vous compilez un fichier .dll ou .exe, le fichier .dll de stockage pour les types générés est lié de manière statique à l’assembly résultant. Ce lien est créé en copiant les définitions de type langage intermédiaire et toutes les ressources managées de l’assembly de stockage dans l’assembly final. Lorsque vous utilisez F# Interactive, le fichier .dll de stockage n’est pas copié, mais est chargé directement dans le processus interactif F#.

Exceptions et diagnostics des fournisseurs de type

Toutes les utilisations de tous les membres des types fournis peuvent lever des exceptions. Dans tous les cas, si un fournisseur de type lève une exception, le compilateur hôte attribue l’erreur à un fournisseur de type spécifique.

  • Les exceptions du fournisseur de type ne doivent jamais entraîner d’erreurs internes du compilateur.

  • Les fournisseurs de type ne peuvent pas signaler d’avertissements.

  • Lorsqu’un fournisseur de type est hébergé dans le compilateur F#, un environnement de développement F# ou F# Interactive, toutes les exceptions de ce fournisseur sont interceptées. La propriété Message est toujours le texte d’erreur et aucun rapport des appels de procédure n’apparaît. Si vous souhaitez lever une exception, vous pouvez lever les exemples suivants : System.NotSupportedException, System.IO.IOException, System.Exception.

Fourniture de types générés

Jusqu’à présent, ce document a expliqué comment fournir des types effacés. Vous pouvez également utiliser le mécanisme de fournisseur de type dans F# pour fournir des types générés, qui sont ajoutés en tant que définitions de type .NET réelles dans le programme des utilisateurs. Vous devez faire référence aux types fournis générés à l’aide d’une définition de type.

open Microsoft.FSharp.TypeProviders

type Service = ODataService<"http://services.odata.org/Northwind/Northwind.svc/">

Le code d’assistance ProvidedTypes-0.2 qui fait partie de la version F# 3.0 n’a qu’une prise en charge limitée pour fournir des types générés. Les instructions suivantes doivent avoir la valeur true pour une définition de type générée :

  • isErased doit être défini sur false.

  • Le type généré doit être ajouté à un ProvidedAssembly() nouvellement construit, qui représente un conteneur pour les fragments de code générés.

  • Le fournisseur doit avoir un assembly qui a un fichier de .dll .NET de stockage réel avec un fichier de .dll correspondant sur le disque.

Règles et limitations

Lorsque vous écrivez des fournisseurs de types, n’oubliez pas les règles et les limitations suivantes.

Les types fournis doivent être accessibles

Tous les types fournis doivent être accessibles à partir des types non imbriqués. Les types non imbriqués sont donnés dans l’appel au constructeur TypeProviderForNamespaces ou dans un appel à AddNamespace. Par exemple, si le fournisseur fournit un type StaticClass.P : T, vous devez vous assurer que T est un type non imbriqué ou qu’il est imbriqué sous un.

Par exemple, certains fournisseurs ont une classe statique telle que DataTypes qui contient les types T1, T2, T3, .... Sinon, l’erreur indique qu’une référence au type T dans l’assembly A a été trouvée, mais le type est introuvable dans cet assembly. Si cette erreur s’affiche, vérifiez que tous vos sous-types sont accessibles à partir des types de fournisseurs. Remarque : les types T1, T2, T3... sont appelés types à la volée. N’oubliez pas de les placer dans un espace de noms accessible ou dans un type parent.

Limitations du mécanisme de fournisseur de type

Le mécanisme de fournisseur de type en F# présente les limitations suivantes :

  • L’infrastructure sous-jacente pour les fournisseurs de type en F# ne prend pas en charge les types génériques fournis ou les méthodes génériques fournies.

  • Le mécanisme ne prend pas en charge les types imbriqués avec des paramètres statiques.

Conseils de développement

Vous trouverez peut-être les conseils suivants utiles pendant le processus de développement :

Exécuter deux instances de Visual Studio

Vous pouvez développer le fournisseur de type dans une instance et tester le fournisseur dans l’autre, car l’IDE de test prend un verrou sur le fichier .dll qui empêche la régénération du fournisseur de type. Par conséquent, vous devez fermer la deuxième instance de Visual Studio pendant que le fournisseur est généré dans la première instance, puis vous devez ouvrir de nouveau la deuxième instance une fois le fournisseur généré.

Déboguer des fournisseurs de type à l’aide d’appels de fsc.exe

Vous pouvez appeler des fournisseurs de type à l’aide des outils suivants :

  • fsc.exe (compilateur de ligne de commande F#)

  • fsi.exe (compilateur de F# Interactive)

  • devenv.exe (Visual Studio)

Vous pouvez souvent déboguer des fournisseurs de type plus facilement en utilisant fsc.exe sur un fichier de script de test (par exemple, script.fsx). Vous pouvez lancer un débogueur à partir d’une invite de commandes.

devenv /debugexe fsc.exe script.fsx

Vous pouvez utiliser la journalisation print-to-stdout.

Voir aussi