Zelfstudie: Een typeprovider maken

Het mechanisme van de typeprovider in F# is een belangrijk onderdeel van de ondersteuning voor uitgebreide programmering van informatie. In deze zelfstudie wordt uitgelegd hoe u uw eigen typeproviders maakt door u door de ontwikkeling van verschillende eenvoudige typen providers te begeleiden om de basisconcepten te illustreren. Zie Typeproviders voor meer informatie over het typeprovidermechanisme in F#.

Het F#-ecosysteem bevat een reeks typen providers voor veelgebruikte internet- en zakelijke gegevensservices. Voorbeeld:

  • FSharp.Data bevat typeproviders voor JSON-, XML-, CSV- en HTML-documentindelingen.

  • SwaggerProvider bevat twee generatieve typeproviders die objectmodel en HTTP-clients genereren voor API's die worden beschreven door OpenApi 3.0- en Swagger 2.0-schema's.

  • FSharp.Data.SqlClient heeft een set typeproviders voor het compileren van het insluiten van T-SQL in F#.

U kunt aangepaste typeproviders maken of u kunt verwijzen naar typeproviders die anderen hebben gemaakt. Uw organisatie kan bijvoorbeeld een gegevensservice hebben die een groot en groeiend aantal benoemde gegevenssets biedt, elk met een eigen stabiel gegevensschema. U kunt een typeprovider maken die de schema's leest en de huidige gegevenssets op een sterk getypte manier aan de programmeur presenteert.

Voordat u begint

Het typeprovidermechanisme is voornamelijk ontworpen voor het injecteren van stabiele gegevens- en serviceinformatieruimten in de F#-programmeerervaring.

Dit mechanisme is niet ontworpen voor het injecteren van informatieruimten waarvan het schema verandert tijdens de uitvoering van het programma op manieren die relevant zijn voor programmalogica. Het mechanisme is ook niet ontworpen voor metaprogrammering binnen de taal, ook al bevat dat domein een aantal geldige toepassingen. U moet dit mechanisme alleen gebruiken indien nodig en wanneer de ontwikkeling van een typeprovider zeer hoge waarde oplevert.

U moet voorkomen dat u een typeprovider schrijft waarin een schema niet beschikbaar is. Op dezelfde manier moet u voorkomen dat u een typeprovider schrijft waarbij een gewone (of zelfs een bestaande) .NET-bibliotheek voldoende is.

Voordat u begint, kunt u de volgende vragen stellen:

  • Hebt u een schema voor uw informatiebron? Zo ja, wat is de toewijzing in het F#- en .NET-typesysteem?

  • Kunt u een bestaande (dynamisch getypte) API gebruiken als uitgangspunt voor uw implementatie?

  • Heeft u en uw organisatie voldoende gebruik van het type provider om het schrijven de moeite waard te maken? Voldoet een normale .NET-bibliotheek aan uw behoeften?

  • Hoeveel verandert uw schema?

  • Verandert het tijdens het coderen?

  • Verandert het tussen coderingssessies?

  • Verandert deze tijdens de uitvoering van het programma?

Typeproviders zijn het meest geschikt voor situaties waarin het schema stabiel is tijdens runtime en tijdens de levensduur van gecompileerde code.

Een provider voor eenvoudig type

Dit voorbeeld is Samples.HelloWorldTypeProvider, vergelijkbaar met de voorbeelden in de examples map van de F#-typeprovider-SDK. De provider maakt een 'typeruimte' beschikbaar die 100 gewiste typen bevat, zoals de volgende code laat zien met behulp van de syntaxis van de F#-handtekening en de details voor iedereen weglaat, behalve Type1. Zie Details over gewiste typen verderop in dit onderwerp voor meer informatie over gewiste typen .

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 =
…

Houd er rekening mee dat de set typen en opgegeven leden statisch bekend is. In dit voorbeeld wordt niet gebruikgemaakt van de mogelijkheid van providers om typen te bieden die afhankelijk zijn van een schema. De implementatie van de typeprovider wordt beschreven in de volgende code en de details worden behandeld in latere secties van dit onderwerp.

Waarschuwing

Er kunnen verschillen zijn tussen deze code en de onlinevoorbeelden.

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()

Als u deze provider wilt gebruiken, opent u een afzonderlijk exemplaar van Visual Studio, maakt u een F#-script en voegt u vervolgens een verwijzing naar de provider vanuit uw script toe met behulp van #r zoals in de volgende code wordt weergegeven:

#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

Zoek vervolgens naar de typen onder de Samples.HelloWorldTypeProvider naamruimte die de typeprovider heeft gegenereerd.

Voordat u de provider opnieuw compileeert, moet u ervoor zorgen dat u alle exemplaren van Visual Studio en F# Interactive hebt gesloten die gebruikmaken van het DLL-bestand van de provider. Anders treedt er een buildfout op omdat de uitvoer-DLL wordt vergrendeld.

Als u fouten in deze provider wilt opsporen met behulp van afdrukinstructies, maakt u een script waarmee een probleem met de provider wordt weergegeven en gebruikt u vervolgens de volgende code:

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

Als u fouten wilt opsporen in deze provider met behulp van Visual Studio, opent u de opdrachtprompt voor Ontwikkelaars voor Visual Studio met beheerdersreferenties en voert u de volgende opdracht uit:

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

Als alternatief opent u Visual Studio, opent u het menu Foutopsporing, kiest Debug/Attach to process…en koppelt u deze aan een ander devenv proces waarin u het script bewerkt. Met deze methode kunt u gemakkelijker bepaalde logica in de typeprovider richten door interactief expressies te typen in het tweede exemplaar (met volledige IntelliSense en andere functies).

U kunt Just My Code-foutopsporing uitschakelen om fouten in gegenereerde code beter te identificeren. Zie Navigeren door code met het foutopsporingsprogramma voor meer informatie over het in- of uitschakelen van deze functie. U kunt ook uitzonderingen voor de eerste kans instellen door het Debug menu te openen en vervolgens ctrl+Alt+E-toetsen te kiezen Exceptions of te kiezen om het Exceptions dialoogvenster te openen. Schakel in dat dialoogvenster onder Common Language Runtime Exceptionshet selectievakje in Thrown .

Implementatie van de typeprovider

In deze sectie wordt u begeleid bij de belangrijkste secties van de implementatie van de provider van het type. Eerst definieert u het type voor de aangepaste typeprovider zelf:

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

Dit type moet openbaar zijn en u moet dit markeren met het kenmerk TypeProvider , zodat de compiler de typeprovider herkent wanneer een afzonderlijk F#-project verwijst naar de assembly die het type bevat. De configuratieparameter is optioneel en bevat, indien aanwezig, contextuele configuratiegegevens voor het exemplaar van de provider van het type dat door de F#-compiler wordt gemaakt.

Vervolgens implementeert u de ITypeProvider-interface . In dit geval gebruikt u het TypeProviderForNamespaces type van de ProvidedTypes API als basistype. Dit helpertype kan een eindige verzameling van gretige opgegeven naamruimten bieden, die elk rechtstreeks een eindig aantal vaste, gretige opgegeven typen bevatten. In deze context genereert de provider gretig typen, zelfs als ze niet nodig of gebruikt zijn.

inherit TypeProviderForNamespaces(config)

Definieer vervolgens lokale privéwaarden die de naamruimte voor de opgegeven typen opgeven en zoek de assembly van de typeprovider zelf. Deze assembly wordt later gebruikt als het logische bovenliggende type van de gewiste typen die worden opgegeven.

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

Maak vervolgens een functie om elk van de typen Type1 op te geven... Type100. Deze functie wordt verderop in dit onderwerp uitgebreid beschreven.

let makeOneProvidedType (n:int) = …

Genereer vervolgens de 100 opgegeven typen:

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

Voeg vervolgens de typen toe als een opgegeven naamruimte:

do this.AddNamespace(namespaceName, types)

Voeg ten slotte een assemblykenmerk toe dat aangeeft dat u een DLL van een typeprovider maakt:

[<assembly:TypeProviderAssembly>]
do()

Eén type en de bijbehorende leden instellen

De makeOneProvidedType functie doet het echte werk van het leveren van een van de typen.

let makeOneProvidedType (n:int) =
…

In deze stap wordt de implementatie van deze functie uitgelegd. Maak eerst het opgegeven type (bijvoorbeeld Type1, wanneer n = 1 of Type57, wanneer 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>)

Let op de volgende punten:

  • Dit opgegeven type wordt gewist. Omdat u aangeeft dat het basistype is obj, worden exemplaren weergegeven als waarden van het type obj in gecompileerde code.

  • Wanneer u een niet-genest type opgeeft, moet u de assembly en naamruimte opgeven. Voor gewiste typen moet de assembly het type providerassembly zelf zijn.

Voeg vervolgens XML-documentatie toe aan het type. Deze documentatie is vertraagd, dat wil gezegd, berekend op aanvraag als de hostcompilator deze nodig heeft.

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

Vervolgens voegt u een opgegeven statische eigenschap toe aan het type:

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

Als u deze eigenschap ophaalt, wordt altijd geëvalueerd naar de tekenreeks 'Hallo!'. Voor GetterCode de eigenschap wordt een F#-aanhalingsteken gebruikt, dat de code vertegenwoordigt die de hostcompilator genereert voor het ophalen van de eigenschap. Zie Code-aanhalingstekens (F#) voor meer informatie over aanhalingstekens.

Xml-documentatie toevoegen aan de eigenschap.

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

Voeg nu de opgegeven eigenschap toe aan het opgegeven type. U moet een opgegeven lid aan één en slechts één type koppelen. Anders is het lid nooit toegankelijk.

t.AddMember staticProp

Maak nu een opgegeven constructor waarvoor geen parameters nodig zijn.

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

De InvokeCode voor de constructor retourneert een F#-aanhalingsteken, dat de code vertegenwoordigt die de hostcompilator genereert wanneer de constructor wordt aangeroepen. U kunt bijvoorbeeld de volgende constructor gebruiken:

new Type10()

Er wordt een exemplaar van het opgegeven type gemaakt met onderliggende gegevens 'De objectgegevens'. De aanhalingstekencode bevat een conversie naar obj omdat dit type het verwijderen van dit opgegeven type is (zoals u hebt opgegeven toen u het opgegeven type hebt gedeclareerd).

Voeg XML-documentatie toe aan de constructor en voeg de opgegeven constructor toe aan het opgegeven type:

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

t.AddMember ctor

Maak een tweede opgegeven constructor die één parameter gebruikt:

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

De InvokeCode voor de constructor retourneert opnieuw een F#-aanhalingsteken, dat de code vertegenwoordigt die de hostcompilator heeft gegenereerd voor een aanroep naar de methode. U kunt bijvoorbeeld de volgende constructor gebruiken:

new Type10("ten")

Er wordt een exemplaar van het opgegeven type gemaakt met onderliggende gegevens 'tien'. Mogelijk hebt u al gemerkt dat de InvokeCode functie een aanhalingsteken retourneert. De invoer voor deze functie is een lijst met expressies, één per constructorparameter. In dit geval is een expressie die de waarde voor één parameter vertegenwoordigt beschikbaar in args[0]. De code voor een aanroep naar de constructor coereert de retourwaarde naar het gewiste type obj. Nadat u de tweede opgegeven constructor aan het type hebt toegevoegd, maakt u een opgegeven exemplaareigenschap:

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

Als u deze eigenschap ophaalt, wordt de lengte van de tekenreeks geretourneerd. Dit is het weergaveobject. De GetterCode eigenschap retourneert een F#-aanhalingsteken dat de code aangeeft die de hostcompilator genereert om de eigenschap op te halen. GetterCode De functie retourneert een InvokeCodeaanhalingsteken. De hostcompilator roept deze functie aan met een lijst met argumenten. In dit geval bevatten de argumenten slechts de enkele expressie die het exemplaar vertegenwoordigt waarop de getter wordt aangeroepen, die u kunt openen met behulp van args[0]. De implementatie van vervolgens wordt gepliceerd in het resultaatcitaat bij het gewiste type objen een cast wordt gebruikt om te voldoen aan het mechanisme van GetterCode de compiler voor het controleren van typen dat het object een tekenreeks is. Het volgende deel van makeOneProvidedType biedt een instantiemethode met één parameter.

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

Maak ten slotte een genest type dat 100 geneste eigenschappen bevat. Het maken van dit geneste type en de eigenschappen ervan worden vertraagd, dat wil gezegd, berekend op aanvraag.

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])

Details over gewiste opgegeven typen

Het voorbeeld in deze sectie bevat alleen gewiste opgegeven typen, die met name handig zijn in de volgende situaties:

  • Wanneer u een provider schrijft voor een informatieruimte die alleen gegevens en methoden bevat.

  • Wanneer u een provider schrijft waarbij nauwkeurige runtime-type semantiek niet essentieel is voor praktisch gebruik van de informatieruimte.

  • Wanneer u een provider schrijft voor een informatieruimte die zo groot en onderling verbonden is dat het technisch niet haalbaar is om echte .NET-typen te genereren voor de informatieruimte.

In dit voorbeeld wordt elk opgegeven type gewist om te typen objen worden alle toepassingen van het type weergegeven als type obj in gecompileerde code. In feite zijn de onderliggende objecten in deze voorbeelden tekenreeksen, maar het type wordt weergegeven zoals System.Object in .NET-gecompileerde code. Net als bij alle toepassingen van typeverwijdering kunt u expliciet boksen, uitpakken en casten gebruiken om gewiste typen te subverten. In dit geval kan een cast-uitzondering die niet geldig is, resulteren in het gebruik van het object. Een providerruntime kan een eigen privéweergavetype definiëren om bescherming te bieden tegen valse representaties. U kunt geen gewiste typen definiëren in F# zelf. Alleen opgegeven typen kunnen worden gewist. U moet de gevolgen begrijpen, zowel praktisch als semantisch, van het gebruik van gewiste typen voor uw typeprovider of een provider die gewiste typen biedt. Een gewist type heeft geen echt .NET-type. Daarom kunt u geen nauwkeurige weerspiegeling van het type uitvoeren en u kunt gewiste typen mogelijk omkeren als u runtimecasts en andere technieken gebruikt die afhankelijk zijn van de exacte semantiek van het runtime-type. Subversie van gewiste typen resulteert vaak in cast-uitzonderingen van het type tijdens runtime.

Weergaven kiezen voor gewiste opgegeven typen

Voor sommige toepassingen van gewiste opgegeven typen is er geen weergave vereist. Het gewiste opgegeven type kan bijvoorbeeld alleen statische eigenschappen en leden en geen constructors bevatten, en geen methoden of eigenschappen retourneren een exemplaar van het type. Als u exemplaren van een gewist opgegeven type kunt bereiken, moet u rekening houden met de volgende vragen:

Wat is het verwijderen van een opgegeven type?

  • Het verwijderen van een opgegeven type is de manier waarop het type wordt weergegeven in gecompileerde .NET-code.

  • Het verwijderen van een opgegeven gewist klassetype is altijd het eerste niet-gewiste basistype in de overnameketen van het type.

  • Het verwijderen van een opgegeven gewist interfacetype is altijd System.Object.

Wat zijn de representaties van een opgegeven type?

  • De set mogelijke objecten voor een gewist opgegeven type worden de weergaven genoemd. In het voorbeeld in dit document zijn de weergaven van alle gewiste opgegeven typen Type1..Type100 altijd tekenreeksobjecten.

Alle representaties van een opgegeven type moeten compatibel zijn met het wissen van het opgegeven type. (Anders geeft de F#-compiler een fout voor het gebruik van de typeprovider of niet-verifieerbare .NET-code die niet geldig is, wordt gegenereerd. Een typeprovider is niet geldig als deze code retourneert die een weergave geeft die niet geldig is.)

U kunt een weergave voor opgegeven objecten kiezen met behulp van een van de volgende benaderingen, die beide zeer gebruikelijk zijn:

  • Als u gewoon een sterk getypte wrapper opgeeft voor een bestaand .NET-type, is het vaak handig voor uw type om dit type te wissen, exemplaren van dat type te gebruiken als representaties of beide. Deze methode is geschikt wanneer de meeste bestaande methoden voor dat type nog steeds zinvol zijn bij het gebruik van de sterk getypte versie.

  • Als u een API wilt maken die aanzienlijk verschilt van een bestaande .NET-API, is het zinvol runtimetypen te maken die het typeverwijdering en representaties voor de opgegeven typen zijn.

In het voorbeeld in dit document worden tekenreeksen gebruikt als weergaven van opgegeven objecten. Vaak is het handig om andere objecten te gebruiken voor representaties. U kunt bijvoorbeeld een woordenlijst gebruiken als een eigenschappentas:

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

Als alternatief kunt u een type definiëren in uw typeprovider die tijdens de runtime wordt gebruikt om de weergave te vormen, samen met een of meer runtimebewerkingen:

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

Opgegeven leden kunnen vervolgens exemplaren van dit objecttype maken:

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

In dit geval kunt u dit type (optioneel) gebruiken als het type wissen door dit type op te geven als de baseType volgende ProvidedTypeDefinition:

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

Belangrijke lessen

In de vorige sectie wordt uitgelegd hoe u een eenvoudige provider voor het wissen van typen maakt die een reeks typen, eigenschappen en methoden biedt. In deze sectie wordt ook het concept van typeverwijdering uitgelegd, waaronder enkele van de voor- en nadelen van het bieden van gewiste typen van een typeprovider en besproken weergaven van gewiste typen.

Een typeprovider die gebruikmaakt van statische parameters

De mogelijkheid om typeproviders te parameteriseren op basis van statische gegevens maakt veel interessante scenario's mogelijk, zelfs in gevallen waarin de provider geen toegang nodig heeft tot lokale of externe gegevens. In deze sectie leert u enkele basistechnieken voor het samenstellen van een dergelijke provider.

Type Ingeschakelde Regex-provider

Stel dat u een typeprovider wilt implementeren voor reguliere expressies die de .NET-bibliotheken Regex verpakken in een interface die de volgende compileertijdgaranties biedt:

  • Controleren of een reguliere expressie geldig is.

  • Benoemde eigenschappen opgeven voor overeenkomsten die zijn gebaseerd op groepsnamen in de reguliere expressie.

In deze sectie wordt beschreven hoe u typeproviders gebruikt om een RegexTyped type te maken dat het reguliere expressiepatroon parameteriseert om deze voordelen te bieden. De compiler rapporteert een fout als het opgegeven patroon niet geldig is en de typeprovider kan de groepen extraheren uit het patroon, zodat u deze kunt openen met behulp van benoemde eigenschappen voor overeenkomsten. Wanneer u een typeprovider ontwerpt, moet u overwegen hoe de weergegeven API eruit moet zien voor eindgebruikers en hoe dit ontwerp wordt omgezet in .NET-code. In het volgende voorbeeld ziet u hoe u een dergelijke API gebruikt om de onderdelen van het netnummer op te halen:

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"

In het volgende voorbeeld ziet u hoe de typeprovider deze aanroepen vertaalt:

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"

Let op de volgende punten:

  • Het standaard regex-type vertegenwoordigt het geparameteriseerde RegexTyped type.

  • De RegexTyped constructor resulteert in een aanroep naar de Regex-constructor, waarbij het argument statisch type voor het patroon wordt doorgegeven.

  • De resultaten van de Match methode worden vertegenwoordigd door het standaardtype Match .

  • Elke benoemde groep resulteert in een opgegeven eigenschap en het openen van de eigenschap resulteert in een gebruik van een indexeerfunctie in de verzameling van Groups een overeenkomst.

De volgende code is de kern van de logica voor het implementeren van een dergelijke provider. In dit voorbeeld wordt het toevoegen van alle leden aan het opgegeven type weggelaten. Zie de betreffende sectie verderop in dit onderwerp voor meer informatie over elk toegevoegd lid.

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 ()

Let op de volgende punten:

  • De typeprovider heeft twee statische parameters: de pattern, die verplicht is en de options, die optioneel zijn (omdat er een standaardwaarde is opgegeven).

  • Nadat de statische argumenten zijn opgegeven, maakt u een exemplaar van de reguliere expressie. Dit exemplaar genereert een uitzondering als de Regex onjuist is ingedeeld en deze fout wordt gerapporteerd aan gebruikers.

  • In de DefineStaticParameters callback definieert u het type dat wordt geretourneerd nadat de argumenten zijn opgegeven.

  • Deze code wordt ingesteld HideObjectMethods op waar, zodat de IntelliSense-ervaring gestroomlijnd blijft. Dit kenmerk zorgt ervoor dat de Equals, GetHashCodeen Finalizeleden GetType worden onderdrukt uit IntelliSense-lijsten voor een opgegeven object.

  • U gebruikt obj als het basistype van de methode, maar u gebruikt een Regex object als runtime-weergave van dit type, zoals in het volgende voorbeeld wordt weergegeven.

  • De aanroep van de Regex constructor genereert een ArgumentException wanneer een reguliere expressie niet geldig is. De compiler onderschept deze uitzondering en rapporteert een foutbericht aan de gebruiker tijdens het compileren of in de Visual Studio-editor. Met deze uitzondering kunnen reguliere expressies worden gevalideerd zonder een toepassing uit te voeren.

Het hierboven gedefinieerde type is nog niet nuttig omdat het geen zinvolle methoden of eigenschappen bevat. Voeg eerst een statische IsMatch methode toe:

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

De vorige code definieert een methode IsMatch, die een tekenreeks als invoer gebruikt en een bool. Het enige lastige deel is het gebruik van het args argument in de InvokeCode definitie. In dit voorbeeld args is een lijst met aanhalingstekens die de argumenten voor deze methode vertegenwoordigen. Als de methode een instantiemethode is, vertegenwoordigt het eerste argument het this argument. Voor een statische methode zijn de argumenten echter alleen de expliciete argumenten voor de methode. Houd er rekening mee dat het type van de opgegeven retourwaarde moet overeenkomen met het opgegeven retourtype (in dit geval bool). Houd er ook rekening mee dat deze code gebruikmaakt van de AddXmlDoc methode om ervoor te zorgen dat de opgegeven methode ook nuttige documentatie bevat, die u kunt opgeven via IntelliSense.

Voeg vervolgens een methode voor vergelijken van exemplaren toe. Deze methode moet echter een waarde van een opgegeven Match type retourneren, zodat de groepen op een sterk getypte manier kunnen worden geopend. U declareert dus eerst het Match type. Omdat dit type afhankelijk is van het patroon dat als een statisch argument is opgegeven, moet dit type worden genest binnen de definitie van het geparameteriseerde type:

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

ty.AddMember matchTy

Vervolgens voegt u één eigenschap toe aan het type Overeenkomst voor elke groep. Tijdens runtime wordt een overeenkomst weergegeven als een Match waarde, dus de aanhaling die de eigenschap definieert, moet de Groups geïndexeerde eigenschap gebruiken om de relevante groep op te halen.

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

Houd er ook rekening mee dat u XML-documentatie toevoegt aan de opgegeven eigenschap. Houd er ook rekening mee dat een eigenschap kan worden gelezen als er een GetterCode functie wordt opgegeven en dat de eigenschap kan worden geschreven als er een SetterCode functie is opgegeven, zodat de resulterende eigenschap alleen-lezen is.

U kunt nu een exemplaarmethode maken die een waarde van dit Match type retourneert:

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

Omdat u een instantiemethode maakt, args[0] vertegenwoordigt u het RegexTyped exemplaar waarop de methode wordt aangeroepen en args[1] is dit het invoerargument.

Geef ten slotte een constructor op, zodat exemplaren van het opgegeven type kunnen worden gemaakt.

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

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

ty.AddMember ctor

De constructor wist slechts het maken van een standaard .NET Regex-exemplaar, dat opnieuw in een vak voor een object wordt geplaatst, omdat obj het opgegeven type wordt gewist. Met deze wijziging werkt het voorbeeld-API-gebruik dat eerder in het onderwerp is opgegeven zoals verwacht. De volgende code is voltooid en definitief:

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 ()

Belangrijke lessen

In deze sectie wordt uitgelegd hoe u een typeprovider maakt die werkt op de statische parameters. De provider controleert de statische parameter en levert bewerkingen op basis van de waarde.

Een typeprovider die wordt ondersteund door lokale gegevens

Vaak wilt u dat typeproviders API's presenteren op basis van niet alleen statische parameters, maar ook informatie van lokale of externe systemen. In deze sectie worden typeproviders besproken die zijn gebaseerd op lokale gegevens, zoals lokale gegevensbestanden.

Eenvoudige CSV-bestandsprovider

Als eenvoudig voorbeeld kunt u een typeprovider overwegen voor toegang tot wetenschappelijke gegevens in CSV-indeling (Door komma's gescheiden waarden). In deze sectie wordt ervan uitgegaan dat de CSV-bestanden een veldnamenrij bevatten, gevolgd door drijvende-kommagegevens, zoals in de volgende tabel wordt geïllustreerd:

Afstand (meter) Tijd (seconde)
50,0 3.7
100,0 5.2
150.0 6.4

In deze sectie wordt beschreven hoe u een type kunt opgeven dat u kunt gebruiken om rijen op te halen met een Distance eigenschap van het type float<meter> en een Time eigenschap van het type float<second>. Ter vereenvoudiging worden de volgende veronderstellingen gemaakt:

  • Kopnamen zijn eenheidsloos of hebben het formulier 'Naam (eenheid)' en bevatten geen komma's.

  • Eenheden zijn alle SI-eenheden (System International) omdat de module FSharp.Data.UnitSystems.SI.UnitNames Module (F#) definieert.

  • Eenheden zijn allemaal eenvoudig (bijvoorbeeld meter) in plaats van samengesteld (bijvoorbeeld meter/seconde).

  • Alle kolommen bevatten drijvendekommagegevens.

Een completere provider zou deze beperkingen versoepen.

Nogmaals de eerste stap is om na te denken over hoe de API eruit moet zien. Gezien een info.csv bestand met de inhoud uit de vorige tabel (in door komma's gescheiden indeling), moeten gebruikers van de provider code kunnen schrijven die lijkt op het volgende voorbeeld:

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

In dit geval moet de compiler deze aanroepen converteren naar iets zoals in het volgende voorbeeld:

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

Voor de optimale vertaling moet de typeprovider een echt CsvFile type definiëren in de assembly van de typeprovider. Typeproviders zijn vaak afhankelijk van een aantal helpertypen en methoden om belangrijke logica te verpakken. Omdat metingen tijdens runtime worden gewist, kunt u een float[] als gewist type voor een rij gebruiken. De compiler behandelt verschillende kolommen als verschillende maateenheidtypen. De eerste kolom in ons voorbeeld heeft bijvoorbeeld het type float<meter>en de tweede kolom.float<second> De gewiste weergave kan echter vrij eenvoudig blijven.

De volgende code toont de kern van de implementatie.

// 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])

Let op de volgende punten over de implementatie:

  • Met overbelaste constructors kan het oorspronkelijke bestand of een bestand met een identiek schema worden gelezen. Dit patroon is gebruikelijk wanneer u een typeprovider schrijft voor lokale of externe gegevensbronnen. Met dit patroon kan een lokaal bestand worden gebruikt als sjabloon voor externe gegevens.

  • U kunt de Waarde TypeProviderConfig gebruiken die wordt doorgegeven aan de constructor van de typeprovider om relatieve bestandsnamen op te lossen.

  • U kunt de AddDefinitionLocation methode gebruiken om de locatie van de opgegeven eigenschappen te definiëren. Als u daarom voor een opgegeven eigenschap gebruikt Go To Definition , wordt het CSV-bestand geopend in Visual Studio.

  • U kunt het ProvidedMeasureBuilder type gebruiken om de SI-eenheden op te zoeken en de relevante float<_> typen te genereren.

Belangrijke lessen

In deze sectie wordt uitgelegd hoe u een typeprovider maakt voor een lokale gegevensbron met een eenvoudig schema dat zich in de gegevensbron zelf bevindt.

Verdergaan

De volgende secties bevatten suggesties voor verdere studie.

Een blik op de gecompileerde code voor gewiste typen

Als u wilt weten hoe het gebruik van de typeprovider overeenkomt met de code die wordt verzonden, bekijkt u de volgende functie met behulp van de HelloWorldTypeProvider functie die eerder in dit onderwerp is gebruikt.

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

Hier volgt een afbeelding van de resulterende code die is gedecompileerd met behulp van 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

Zoals in het voorbeeld wordt weergegeven, zijn alle vermeldingen van het type Type1 en de InstanceProperty eigenschap gewist, waardoor alleen bewerkingen voor de betrokken runtimetypen worden achtergelaten.

Ontwerp- en naamconventies voor typeproviders

Bekijk de volgende conventies bij het ontwerpen van typeproviders.

Providers voor Verbinding maken iviteitsprotocollen in het algemeen, namen van de meeste provider-DLL's voor protocollen voor gegevens- en serviceconnectiviteit, zoals OData- of SQL-verbindingen, moeten eindigen op TypeProvider of TypeProviders. Gebruik bijvoorbeeld een DLL-naam die lijkt op de volgende tekenreeks:

Fabrikam.Management.BasicTypeProviders.dll

Zorg ervoor dat uw opgegeven typen lid zijn van de bijbehorende naamruimte en geef het connectiviteitsprotocol aan dat u hebt geïmplementeerd:

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

Hulpprogrammaproviders voor algemene codering. Voor een provider van hulpprogrammatypen, zoals die voor reguliere expressies, kan de typeprovider deel uitmaken van een basisbibliotheek, zoals in het volgende voorbeeld wordt weergegeven:

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

In dit geval wordt het opgegeven type weergegeven op een geschikt punt volgens normale .NET-ontwerpconventies:

  open Fabrikam.Core.Text.RegexTyped

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

Singleton-gegevensbronnen. Sommige typeproviders maken verbinding met één toegewezen gegevensbron en bieden alleen gegevens. In dit geval moet u het TypeProvider achtervoegsel verwijderen en normale conventies gebruiken voor .NET-naamgeving:

#r "Fabrikam.Data.Freebase.dll"

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

Zie de GetConnection ontwerpconventie die verderop in dit onderwerp wordt beschreven voor meer informatie.

Ontwerppatronen voor typeproviders

In de volgende secties worden ontwerppatronen beschreven die u kunt gebruiken bij het ontwerpen van typeproviders.

Het ontwerppatroon Get Verbinding maken ion

De meeste typeproviders moeten worden geschreven om het GetConnection patroon te gebruiken dat wordt gebruikt door de typeproviders in FSharp.Data.TypeProviders.dll, zoals in het volgende voorbeeld wordt weergegeven:

#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

Typeproviders ondersteund door externe gegevens en services

Voordat u een typeprovider maakt die wordt ondersteund door externe gegevens en services, moet u rekening houden met een reeks problemen die inherent zijn aan verbonden programmering. Deze problemen omvatten de volgende overwegingen:

  • schematoewijzing

  • leefbaarheid en ongeldigheid in aanwezigheid van schemawijziging

  • schemacaching

  • asynchrone implementaties van bewerkingen voor gegevenstoegang

  • ondersteunende query's, inclusief LINQ-query's

  • referenties en verificatie

In dit onderwerp worden deze problemen niet verder besproken.

Aanvullende ontwerptechnieken

Wanneer u uw eigen typeproviders schrijft, kunt u de volgende aanvullende technieken gebruiken.

Typen en leden op aanvraag maken

De ProvidedType-API heeft vertraagde versies van AddMember.

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

Deze versies worden gebruikt om ruimten op aanvraag van typen te maken.

Matrixtypen en algemene type instantiëringen bieden

U maakt opgegeven leden (waarvan de handtekeningen matrixtypen, byref-typen en instantiëringen van algemene typen bevatten) met behulp van de normale MakeArrayType, MakePointerTypeen MakeGenericType op elk exemplaar van Type, inclusief ProvidedTypeDefinitions.

Notitie

In sommige gevallen moet u mogelijk de helper gebruiken in ProvidedTypeBuilder.MakeGenericType. Zie de documentatie voor de Type Provider SDK voor meer informatie.

Aantekeningen voor maateenheid verleent

De ProvidedTypes-API biedt helpers voor het leveren van metingen. Als u bijvoorbeeld het type float<kg>wilt opgeven, gebruikt u de volgende code:

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

Gebruik de volgende code om het type Nullable<decimal<kg/m^2>>op te geven:

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

Toegang tot project-lokale of script-lokale resources

Elk exemplaar van een typeprovider kan tijdens de bouw een TypeProviderConfig waarde krijgen. Deze waarde bevat de oplossingsmap voor de provider (de projectmap voor de compilatie of de map die een script bevat), de lijst met assembly's waarnaar wordt verwezen en andere informatie.

Nietigverklaring

Providers kunnen ongeldige signalen genereren om de F#-taalservice op de hoogte te stellen dat de schemaveronderstellingen mogelijk zijn gewijzigd. Wanneer er een ongeldige bewerking optreedt, wordt een typecontrole opnieuw uitgevoerd als de provider wordt gehost in Visual Studio. Dit signaal wordt genegeerd wanneer de provider wordt gehost in F# Interactive of door de F#-compiler (fsc.exe).

Schemagegevens opslaan in cache

Providers moeten vaak de toegang tot schemagegevens in de cache opslaan. De gegevens in de cache moeten worden opgeslagen met behulp van een bestandsnaam die wordt opgegeven als een statische parameter of als gebruikersgegevens. Een voorbeeld van schemacaching is de LocalSchemaFile parameter in de typeproviders in de FSharp.Data.TypeProviders assembly. In de implementatie van deze providers stuurt deze statische parameter de typeprovider om de schemagegevens in het opgegeven lokale bestand te gebruiken in plaats van toegang te krijgen tot de gegevensbron via het netwerk. Als u schemagegevens in de cache wilt gebruiken, moet u ook de statische parameter ForceUpdatefalseinstellen op . U kunt een vergelijkbare techniek gebruiken om online- en offlinegegevenstoegang in te schakelen.

Backing Assembly

Wanneer u een .dll of .exe bestand compileert, wordt het backing-.dll-bestand voor gegenereerde typen statisch gekoppeld aan de resulterende assembly. Deze koppeling wordt gemaakt door de definities van het type Tussentaal (IL) en alle beheerde resources van de backing-assembly naar de uiteindelijke assembly te kopiëren. Wanneer u F# Interactive gebruikt, wordt het backing-.dll bestand niet gekopieerd en wordt het in plaats daarvan rechtstreeks in het interactieve F#-proces geladen.

Uitzonderingen en diagnostische gegevens van typeproviders

Alle toepassingen van alle leden van de opgegeven typen kunnen uitzonderingen genereren. Als een typeprovider in alle gevallen een uitzondering genereert, wordt de fout door de hostcompilator aan een specifieke typeprovider toegewezen.

  • Typeprovideruitzondering mag nooit leiden tot interne compilerfouten.

  • Typeproviders kunnen geen waarschuwingen rapporteren.

  • Wanneer een typeprovider wordt gehost in de F#-compiler, een F#-ontwikkelomgeving of F# Interactive, worden alle uitzonderingen van die provider gevangen. De eigenschap Bericht is altijd de fouttekst en er wordt geen stacktracering weergegeven. Als u een uitzondering gaat genereren, kunt u de volgende voorbeelden genereren: System.NotSupportedException, System.IO.IOException, System.Exception.

Gegenereerde typen verleent

Tot nu toe heeft dit document uitgelegd hoe u gewiste typen kunt opgeven. U kunt ook het typeprovidermechanisme in F# gebruiken om gegenereerde typen te bieden, die worden toegevoegd als echte .NET-typedefinities in het programma van de gebruikers. U moet verwijzen naar gegenereerde opgegeven typen met behulp van een typedefinitie.

open Microsoft.FSharp.TypeProviders

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

De helpercode ProvidedTypes-0.2 die deel uitmaakt van de F# 3.0-release heeft slechts beperkte ondersteuning voor het leveren van gegenereerde typen. De volgende instructies moeten waar zijn voor een gegenereerde typedefinitie:

  • isErased moet zijn ingesteld op false.

  • Het gegenereerde type moet worden toegevoegd aan een nieuw samengesteld ProvidedAssembly()type, dat een container vertegenwoordigt voor gegenereerde codefragmenten.

  • De provider moet een assembly hebben met een daadwerkelijk back-upbestand van .NET .dll met een overeenkomend .dll bestand op schijf.

Regels en beperkingen

Houd bij het schrijven van typeproviders rekening met de volgende regels en beperkingen.

Opgegeven typen moeten bereikbaar zijn

Alle opgegeven typen moeten bereikbaar zijn vanaf de niet-geneste typen. De niet-geneste typen worden gegeven in de aanroep van de TypeProviderForNamespaces constructor of een aanroep naar AddNamespace. Als de provider bijvoorbeeld een type StaticClass.P : Tlevert, moet u ervoor zorgen dat T een niet-genest type is of onder één is genest.

Sommige providers hebben bijvoorbeeld een statische klasse, zoals DataTypes die deze T1, T2, T3, ... typen bevatten. Anders wordt in de fout aangegeven dat er een verwijzing naar het type T in assembly A is gevonden, maar het type kan niet worden gevonden in die assembly. Als deze fout wordt weergegeven, controleert u of al uw subtypen kunnen worden bereikt vanuit de providertypen. Opmerking: deze T1, T2, T3... typen worden de on-the-fly types genoemd. Vergeet niet om ze in een toegankelijke naamruimte of een bovenliggend type te plaatsen.

Beperkingen van het type providermechanisme

Het typeprovidermechanisme in F# heeft de volgende beperkingen:

  • De onderliggende infrastructuur voor typeproviders in F# biedt geen ondersteuning voor opgegeven algemene typen of opgegeven algemene methoden.

  • Het mechanisme biedt geen ondersteuning voor geneste typen met statische parameters.

Tips voor ontwikkeling

Mogelijk vindt u de volgende tips nuttig tijdens het ontwikkelingsproces:

Twee exemplaren van Visual Studio uitvoeren

U kunt de typeprovider in het ene exemplaar ontwikkelen en de provider in het andere testen omdat de test-IDE een vergrendeling op het .dll-bestand neemt waardoor de typeprovider niet opnieuw kan worden opgebouwd. Daarom moet u het tweede exemplaar van Visual Studio sluiten terwijl de provider is gebouwd in het eerste exemplaar en vervolgens moet u het tweede exemplaar opnieuw openen nadat de provider is gebouwd.

Foutopsporingsproviders met behulp van aanroepen van fsc.exe

U kunt typeproviders aanroepen met behulp van de volgende hulpprogramma's:

  • fsc.exe (de F#-opdrachtregelcompilator)

  • fsi.exe (de interactieve F#-compiler)

  • devenv.exe (Visual Studio)

U kunt vaak eenvoudig foutopsporingsproviders uitvoeren met behulp van fsc.exe in een testscriptbestand (bijvoorbeeld script.fsx). U kunt een foutopsporingsprogramma starten vanaf een opdrachtprompt.

devenv /debugexe fsc.exe script.fsx

U kunt logboekregistratie voor afdrukken naar stdout gebruiken.

Zie ook