Freigeben über


Tutorial: Erstellen eines Typanbieters

Der Typanbietermechanismus in F# ist ein wesentlicher Bestandteil der Unterstützung für datenreiche Programmierung. In diesem Lernprogramm wird erläutert, wie Sie eigene Typanbieter erstellen können, indem Schritt für Schritt mehrere einfache Typanbieter entwickelt und an diesen die grundlegenden Konzepte veranschaulicht werden. Weitere Informationen zum Typanbietermechanismus in F# finden Sie unter "Typanbieter".

Das F#-Ökosystem enthält eine Reihe von Typanbietern für häufig verwendete Internet- und Unternehmensdatendienste. Beispiel:

  • FSharp.Data enthält Typanbieter für JSON-, XML-, CSV- und HTML-Dokumentformate.

  • SwaggerProvider enthält zwei generative Typanbieter, die Objektmodell- und HTTP-Clients für APIs generieren, die von OpenApi 3.0- und Swagger 2.0-Schemas beschrieben werden.

  • FSharp.Data.SqlClient verfügt über eine Reihe von Typanbietern für die kompilierte Einbettung von T-SQL in F#.

Sie können eigene benutzerdefinierte Typanbieter erstellen oder auf Typanbieter verweisen, die von anderen Entwicklern erstellt wurden. Beispielsweise könnte Ihre Organisation über einen Datendienst verfügen, der eine große und wachsende Anzahl von benannten Datensätzen bereitstellt, die jeweils ein eigenes stabiles Datenschema aufweisen. Sie können einen Typanbieter erstellen, der die Schemas liest und die aktuellen Datasets dem Programmierer auf stark typierte Weise darstellt.

Bevor Sie beginnen

Der Typanbietermechanismus wurde in erster Linie für das Einfügen stabiler Daten- und Dienstinformationsplätze in die F#-Programmierumgebung entwickelt.

Dieser Mechanismus wurde nicht für das Einfügen von Informationsplätzen entwickelt, deren Schemaänderungen während der Programmausführung auf eine Weise vorgenommen werden, die für die Programmlogik relevant sind. Außerdem ist der Mechanismus nicht für die intrasprachliche Metaprogrammierung konzipiert, obwohl diese Domäne einige gültige Verwendungen enthält. Sie sollten diesen Mechanismus nur verwenden, wenn dies erforderlich ist und sich durch die Entwicklung eines Typanbieters ein deutlicher Mehrwert für die Programmierung erreichen lässt.

Schreiben Sie keinen Typanbieter in Situationen, in denen kein Schema verfügbar ist. Ebenso sollten Sie das Schreiben eines Typanbieters vermeiden, bei dem eine normale (oder sogar eine vorhandene) .NET-Bibliothek ausreichen würde.

Bevor Sie beginnen, können Sie die folgenden Fragen stellen:

  • Haben Sie ein Schema für Ihre Informationsquelle? Wenn ja, was ist die Zuordnung zum F#- und .NET-Typsystem?

  • Können Sie eine vorhandene (dynamisch typierte) API als Ausgangspunkt für Ihre Implementierung verwenden?

  • Haben Sie und die Organisation genügend Verwendungsfälle definieren können, sodass der Aufwand für die Entwicklung eines Typanbieters gerechtfertigt ist? Würde eine normale .NET-Bibliothek Ihre Anforderungen erfüllen?

  • Wie viel ändert sich Ihr Schema?

  • Ändert sich dies während der Codierung?

  • Ändert sich dies zwischen Codierungssitzungen?

  • Ändert sich dies während der Programmausführung?

Typanbieter eignen sich am besten für Situationen, in denen das Schema zur Laufzeit und während der Lebensdauer des kompilierten Codes stabil ist.

Ein einfacher Typanbieter

Dieses Beispiel ist Samples.HelloWorldTypeProvider, ähnlich wie die Beispiele im examples Verzeichnis des F#-Typanbieter-SDK. Der Anbieter stellt einen "Typraum" zur Verfügung, der 100 gelöschte Typen enthält, wie der folgende Code zeigt, indem die F#-Signatursyntax verwendet wird und die Details für alle außer Type1ausgelassen werden. Weitere Informationen zu gelöschten Typen finden Sie unter Details zu gelöschten bereitgestellten Typen weiter unten in diesem Thema.

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

Beachten Sie, dass der Satz der bereitgestellten Typen und Member statisch verfügbar gemacht wird. In diesem Beispiel wird die Fähigkeit von Anbietern nicht genutzt, Typen bereitzustellen, die von einem Schema abhängig sind. Die Implementierung des Typanbieters wird im folgenden Code beschrieben, und die Details werden in späteren Abschnitten dieses Themas behandelt.

Warnung

Es kann Unterschiede zwischen diesem Code und den Onlinebeispielen geben.

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

Um diesen Anbieter zu verwenden, öffnen Sie eine separate Instanz von Visual Studio, erstellen Sie ein F#-Skript, und fügen Sie dann einen Verweis auf den Anbieter aus Ihrem Skript hinzu, indem Sie #r verwenden, wie der folgende Code zeigt:

#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

Suchen Sie dann nach den Typen unter dem Samples.HelloWorldTypeProvider Namespace, den der Typanbieter generiert hat.

Bevor Sie den Anbieter erneut kompilieren, stellen Sie sicher, dass Sie alle Instanzen von Visual Studio und F# Interactive geschlossen haben, die die Anbieter-DLL verwenden. Andernfalls tritt ein Buildfehler auf, da die Ausgabe-DLL gesperrt ist.

Um diesen Anbieter mithilfe von Druckanweisungen zu debuggen, erstellen Sie ein Skript, das ein Problem mit dem Anbieter verfügbar macht, und verwenden Sie dann den folgenden Code:

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

Um diesen Anbieter mithilfe von Visual Studio zu debuggen, öffnen Sie die Entwickler-Eingabeaufforderung für Visual Studio mit Administratoranmeldeinformationen, und führen Sie den folgenden Befehl aus:

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

Öffnen Sie alternativ Visual Studio, öffnen Sie das Menü "Debuggen", wählen Sie Debug/Attach to process… aus, und verbinden Sie sich mit einem anderen devenv Prozess, in dem Sie Ihr Skript bearbeiten. Mithilfe dieser Methode können Sie eine bestimmte Logik im Typanbieter einfacher ansprechen, indem Sie Ausdrücke interaktiv in die zweite Instanz eingeben (mit vollständigem IntelliSense und anderen Features).

Sie können das Debuggen von Just My Code deaktivieren, um Fehler im generierten Code besser zu identifizieren. Informationen zum Aktivieren oder Deaktivieren dieses Features finden Sie unter Navigieren durch Code mit dem Debugger. Sie können auch das Abfangen von Ausnahmen mit erster Chance einstellen, indem Sie das Debug Menü öffnen und dann Exceptions auswählen oder STRG+ALT+E drücken, um das Exceptions Dialogfeld zu öffnen. Aktivieren Sie in diesem Dialogfeld unter Common Language Runtime Exceptions das Kontrollkästchen Thrown.

Implementierung des Typanbieters

Dieser Abschnitt führt Sie durch die Hauptabschnitte der Typanbieterimplementierung. Zunächst definieren Sie den Typ für den benutzerdefinierten Typanbieter selbst:

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

Dieser Typ muss öffentlich sein, und Sie müssen ihn mit dem TypeProvider-Attribut markieren, damit der Compiler den Typanbieter erkennt, wenn ein separates F#-Projekt auf die Assembly verweist, die den Typ enthält. Der Konfigurationsparameter ist optional und enthält ggf. Kontextkonfigurationsinformationen für die Typanbieterinstanz, die der F#-Compiler erstellt.

Als Nächstes implementieren Sie die ITypeProvider-Schnittstelle . In diesem Fall verwenden Sie den TypeProviderForNamespaces Typ aus der ProvidedTypes API als Basistyp. Dieser Hilfstyp kann eine endliche Auflistung vorzeitig bereitgestellter Namespaces bereitstellen, von denen jeder direkt eine begrenzte Zahl fester, vorzeitig bereitgestellter Typen enthält. In diesem Zusammenhang generiert der Anbieter eifrig Typen, auch wenn sie nicht benötigt oder verwendet werden.

inherit TypeProviderForNamespaces(config)

Als Nächstes definieren Sie lokale private Werte, um den Namespace für die bereitgestellten Typen anzugeben, und suchen die eigentliche Typanbieterassembly. Diese Assembly wird später als logisch übergeordneter Typ der bereitgestellten gelöschten Typen verwendet.

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

Erstellen Sie als Nächstes eine Funktion, um die einzelnen Typen "Type1" bereitzustellen... Typ100. Diese Funktion wird weiter unten in diesem Thema ausführlicher erläutert.

let makeOneProvidedType (n:int) = …

Generieren Sie als Nächstes die bereitgestellten 100 Typen:

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

Als Nächstes fügen Sie die Typen als bereitgestellten Namespace hinzu:

do this.AddNamespace(namespaceName, types)

Fügen Sie schließlich ein Assembly-Attribut hinzu, das angibt, dass Sie eine Typanbieter-DLL erstellen:

[<assembly:TypeProviderAssembly>]
do()

Bereitstellen eines einzelnen Typs und seiner Member

Die makeOneProvidedType Funktion führt die eigentliche Arbeit aus, um einen der Typen bereitzustellen.

let makeOneProvidedType (n:int) =
…

In diesem Schritt wird die Implementierung dieser Funktion erläutert. Erstellen Sie zunächst den bereitgestellten Typ (z. B. Type1, wenn n = 1 oder Type57, wenn 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>)

Beachten Sie die folgenden Punkte:

  • Dieser bereitgestellte Typ wird gelöscht. Da Sie angeben, dass der Basistyp lautet obj, werden Instanzen als Werte des Typs "obj " im kompilierten Code angezeigt.

  • Wenn Sie einen nicht geschachtelten Typ angeben, müssen Sie die Assembly und den Namespace angeben. Bei gelöschten Typen sollte die Assembly die Typanbieterassembly selbst sein.

Fügen Sie als Nächstes dem Typ XML-Dokumentation hinzu. Diese Dokumentation wird verzögert erstellt, d. h. bei Bedarf berechnet, wenn der Host-Compiler sie benötigt.

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

Als Nächstes fügen Sie dem Typ eine bereitgestellte statische Eigenschaft hinzu:

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

Beim Abrufen dieser Eigenschaft wird immer die Zeichenfolge "Hello!" zurückgegeben. Der GetterCode für die Eigenschaft verwendet ein F#-Zitat, das den vom Hostcompiler generierten Code zum Abrufen der Eigenschaft darstellt. Weitere Informationen zu Zitaten finden Sie unter Codezitate (F#).

Fügen Sie der Eigenschaft eine XML-Dokumentation hinzu.

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

Fügen Sie nun die bereitgestellte Eigenschaft an den angegebenen Typ an. Sie müssen einen bereitgestellten Member an genau einen Typ anfügen. Andernfalls kann auf das Mitglied nie zugegriffen werden.

t.AddMember staticProp

Erstellen Sie nun einen bereitgestellten Konstruktor, der keine Parameter akzeptiert.

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

Der InvokeCode Konstruktor gibt ein F#-Anführungszeichen zurück, das den Code darstellt, den der Hostcompiler generiert, wenn der Konstruktor aufgerufen wird. Sie können beispielsweise den folgenden Konstruktor verwenden:

new Type10()

Eine Instanz des bereitgestellten Typs wird mit zugrunde liegenden Daten "Die Objektdaten" erstellt. Der zitierte Code enthält eine Konvertierung in obj , da dieser Typ die Löschung dieses angegebenen Typs ist (wie Sie angegeben haben, wenn Sie den angegebenen Typ deklariert haben).

Fügen Sie dem Konstruktor XML-Dokumentation hinzu, und fügen Sie den bereitgestellten Konstruktor zum bereitgestellten Typ hinzu:

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

t.AddMember ctor

Erstellen Sie einen zweiten bereitgestellten Konstruktor, der einen Parameter verwendet:

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

Erneut gibt der InvokeCode-Konstruktor eine F#-Quotation zurück, die den Code darstellt, den der Hostcompiler bei einem Methodenaufruf generiert hat. Sie können beispielsweise den folgenden Konstruktor verwenden:

new Type10("ten")

Eine Instanz des bereitgestellten Typs wird mit zugrunde liegenden Daten "ten" erstellt. Möglicherweise haben Sie bereits bemerkt, dass die InvokeCode Funktion ein Zitat zurückgibt. Die Eingabe für diese Funktion ist eine Liste von Ausdrücken, eine pro Konstruktorparameter. In diesem Fall steht ein Ausdruck, der den wert des einzelnen Parameters darstellt, in args[0]. Der Code für einen Aufruf des Konstruktors wandelt den Rückgabewert in den gelöschten Typ obj um. Nachdem Sie den zweiten bereitgestellten Konstruktor zum Typ hinzugefügt haben, stellt man eine Instanz-Eigenschaft bereit:

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

Wenn Sie diese Eigenschaft abrufen, wird die Länge der Zeichenfolge zurückgegeben, bei der es sich um das Darstellungsobjekt handelt. Die GetterCode Eigenschaft gibt ein F#-Anführungszeichen zurück, das den Code angibt, den der Hostcompiler generiert, um die Eigenschaft abzurufen. Wie InvokeCode gibt die GetterCode-Funktion ein Zitat zurück. Der Hostcompiler ruft diese Funktion mit einer Liste von Argumenten auf. In diesem Fall enthalten die Argumente nur den einzelnen Ausdruck, der die Instanz darstellt, auf die der Getter aufgerufen wird, und auf die Sie mit args[0] zugreifen können. Die Implementierung von GetterCode wird dann in das Ergebniszitat des gelöschten Typs obj eingespleißt. Mit einer Umwandlung wird der Compilermechanismus zum Überprüfen von Typen erfüllt, dass das Objekt eine Zeichenfolge ist. Der nächste Teil von makeOneProvidedType stellt eine Instanzmethode mit einem Parameter bereit.

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

Erstellen Sie schließlich einen geschachtelten Typ, der 100 geschachtelte Eigenschaften enthält. Die Erstellung dieses geschachtelten Typs und seiner Eigenschaften wird verzögert, d. h., er wird erst bei Bedarf berechnet.

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 über gelöschte bereitgestellte Typen

Das Beispiel in diesem Abschnitt enthält nur gelöschte bereitgestellte Typen, die in den folgenden Situationen besonders nützlich sind:

  • Wenn Sie einen Anbieter für einen Informationsraum schreiben, der nur Daten und Methoden enthält.

  • Wenn Sie einen Anbieter schreiben, bei dem zur Laufzeit für die Verwendung des Informationsraums keine exakte Typsemantik erforderlich ist.

  • Wenn Sie einen Anbieter für einen Informationsraum schreiben, der so groß und miteinander verbunden ist, dass es technisch nicht machbar ist, echte .NET-Typen für den Informationsraum zu generieren.

In diesem Beispiel wird jeder bereitgestellte Typ zum Typ objgelöscht, und alle Verwendungsmöglichkeiten des Typs werden als Typ obj im kompilierten Code angezeigt. Tatsächlich sind die zugrunde liegenden Objekte in diesen Beispielen Zeichenfolgen, der Typ wird jedoch wie System.Object im kompilierten .NET-Code angezeigt. Wie bei jeder Verwendung der Typlöschung können Sie explizites Boxing und Unboxing sowie Umwandlungen verwenden, um gelöschte Typen zu unterlaufen. In diesem Fall kann eine ungültige Umwandlungsausnahme auftreten, wenn das Objekt verwendet wird. Eine Anbieterlaufzeit kann einen eigenen privaten Darstellungstyp definieren, um vor falschen Darstellungen zu schützen. Sie können nicht gelöschte Typen in F# selbst definieren. Es können nur angegebene Typen gelöscht werden. Sie müssen sich darüber im Klaren sein, welche Auswirkungen, sowohl praktisch als auch semantisch, die Verwendung von gelöschten Typen für Ihren Typanbieter hat, im Vergleich zu einem Anbieter, der selbst gelöschte Typen bereitstellt. Ein gelöschter Typ hat keinen echten .NET-Typ. Daher können Sie keine genaue Reflektion über den Typ ausführen, und Sie unterlaufen möglicherweise gelöschte Typen, wenn Sie zur Laufzeit Umwandlungen oder andere Techniken verwenden, die zur Laufzeit eine exakte Typsemantik erfordern. Die Subversion gelöschter Typen führt zur Laufzeit häufig zu Ausnahmen bei der Typumwandlung.

Auswählen von Darstellungen für gelöschte bereitgestellte Typen

Für einige Verwendungen von gelöschten bereitgestellten Typen ist keine Darstellung erforderlich. Der gelöschte bereitgestellte Typ kann z. B. nur statische Eigenschaften und Member und keine Konstruktoren enthalten, und keine Methoden oder Eigenschaften würden eine Instanz des Typs zurückgeben. Wenn Sie Zugriff auf Instanzen eines gelöschten bereitgestellten Typs haben, müssen Sie die folgenden Fragen berücksichtigen:

Was ist die Löschung eines bereitgestellten Typs?

  • Die Löschung eines bereitgestellten Typs bezeichnet die Darstellung des Typs im kompilierten .NET-Code.

  • Die Löschung eines bereitgestellten gelöschten Klassentyps ist immer der erste nicht gelöschte Basistyp in die Vererbungskette des Typs.

  • Die Löschung eines bereitgestellten gelöschten Schnittstellentyps ist immer System.Object.

Was sind die Darstellungen eines angegebenen Typs?

  • Der Satz möglicher Objekte für einen gelöschten bereitgestellten Typ wird als seine Darstellungen bezeichnet. Im Beispiel in diesem Dokument sind die Darstellungen aller gelöschten bereitgestellten Typen Type1..Type100 immer Zeichenfolgenobjekte.

Alle Darstellungen eines angegebenen Typs müssen mit der Löschung des angegebenen Typs kompatibel sein. (Andernfalls gibt der F#-Compiler einen Fehler bei der Verwendung des Typanbieters aus, oder es wird nicht verifizierbarer .NET-Code generiert, der ungültig ist. Ein Typanbieter ist ungültig, wenn er Code zurückgibt, der eine ungültige Darstellung ergibt.)

Sie können eine Darstellung für bereitgestellte Objekte auswählen, indem Sie eine der folgenden Ansätze verwenden, die beide sehr häufig sind:

  • Wenn Sie einfach einen stark typisierten Wrapper für einen vorhandenen .NET-Typ bereitstellen, ist es meist sinnvoll, diesen .NET-Typ als Löschung für den Typ zu wählen, Instanzen dieses Typs als Darstellungen zu verwenden oder beide Wege zu wählen. Dieser Ansatz eignet sich, wenn bei Verwendung der stark typisierten Version die meisten vorhandenen Methoden für diesen Typ weiterhin sinnvoll sind.

  • Wenn Sie eine API erstellen möchten, die sich erheblich von einer vorhandenen .NET-API unterscheidet, ist es sinnvoll, Laufzeittypen zu erstellen, bei denen es sich um die Typlöschung und Darstellung der bereitgestellten Typen handelt.

Im Beispiel in diesem Dokument werden Zeichenfolgen als Darstellungen der bereitgestellten Objekte verwendet. Häufig kann es sinnvoll sein, andere Objekte für Darstellungen zu verwenden. Beispielsweise können Sie ein Wörterbuch als Eigenschaftensammlung verwenden:

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

Alternativ können Sie einen Typ in Ihrem Typanbieter definieren, der zur Laufzeit verwendet wird, um die Darstellung zusammen mit einem oder mehreren Laufzeitvorgängen zu bilden:

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

Bereitgestellte Member können dann Instanzen dieses Objekttyps erstellen:

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

In diesem Fall können Sie diesen Typ (optional) als Typlöschung verwenden, indem Sie diesen Typ als baseType beim Erstellen des ProvidedTypeDefinition angeben.

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

Wichtige Lektionen

Im vorherigen Abschnitt wurde erläutert, wie Sie einen einfachen Löschtypanbieter erstellen, der eine Reihe von Typen, Eigenschaften und Methoden bereitstellt. In diesem Abschnitt wurde auch das Konzept der Typlöschung erläutert, einschließlich einiger der Vor- und Nachteile der Bereitstellung von gelöschten Typen von einem Typanbieter und erläuterte Darstellungen von gelöschten Typen.

Ein Typanbieter, der statische Parameter verwendet

Die Möglichkeit zum Parametrisieren von Typanbietern durch statische Daten ermöglicht viele interessante Szenarien, auch wenn der Anbieter nicht auf lokale oder Remotedaten zugreifen muss. In diesem Abschnitt lernen Sie einige grundlegende Techniken zum Zusammenstellen eines solchen Anbieters kennen.

Typgeprüfter Regex-Anbieter

Stellen Sie sich vor, Sie möchten einen Typanbieter für reguläre Ausdrücke implementieren, die die .NET-Bibliotheken Regex in eine Schnittstelle umschließen, die die folgenden Kompilierungszeitgarantien bereitstellt:

  • Überprüfen, ob ein regulärer Ausdruck gültig ist.

  • Bereitstellen benannter Eigenschaften für Übereinstimmungen, die auf vorhandenen Gruppennamen im regulären Ausdruck basieren.

In diesem Abschnitt wird gezeigt, wie Sie Mithilfe von Typanbietern einen RegexTyped Typ erstellen, den das Muster für reguläre Ausdrücke parametrisiert, um diese Vorteile bereitzustellen. Der Compiler meldet einen Fehler, wenn das angegebene Muster ungültig ist, und der Typanbieter kann die Gruppen aus dem Muster extrahieren, sodass Sie auf sie zugreifen können, indem Sie benannte Eigenschaften für Übereinstimmungen verwenden. Wenn Sie einen Typanbieter entwerfen, sollten Sie überlegen, wie die verfügbar gemachte API für Endbenutzer aussehen soll und wie dieses Design in .NET-Code übersetzt wird. Das folgende Beispiel zeigt, wie Sie eine solche API verwenden, um die Komponenten der Ortsvorwahl abzurufen:

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"

Im folgenden Beispiel wird gezeigt, wie der Typanbieter diese Aufrufe übersetzt:

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"

Beachten Sie die folgenden Punkte:

  • Der Standard-Regex-Typ stellt den parametrisierten RegexTyped Typ dar.

  • Der RegexTyped Konstruktor führt zu einem Aufruf des Regex-Konstruktors und übergibt das statische Typargument für das Muster.

  • Die Ergebnisse der Match Methode werden durch den Standardtyp Match dargestellt.

  • Für jede benannte Gruppe wird eine bereitgestellte Eigenschaft erzeugt, und bei einem Zugriff auf die Eigenschaft wird ein Indexer verwendet, um eine Groups-Auflistung der Übereinstimmungen abzurufen.

Der folgende Code ist der Kern der Logik, um einen solchen Provider zu implementieren, und in diesem Beispiel wurde das Hinzufügen aller Mitglieder zum bereitgestellten Typ ausgelassen. Informationen zu den einzelnen hinzugefügten Membern finden Sie im entsprechenden Abschnitt weiter unten in diesem Thema.

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

Beachten Sie die folgenden Punkte:

  • Der Typanbieter verwendet zwei statische Parameter: die pattern, die obligatorisch ist, und die options, die optional sind (da ein Standardwert bereitgestellt wird).

  • Nachdem die statischen Argumente angegeben wurden, erstellen Sie eine Instanz des regulären Ausdrucks. Diese Instanz löst eine Ausnahme aus, wenn regex falsch formatiert ist, und dieser Fehler wird benutzern gemeldet.

  • Innerhalb des DefineStaticParameters Rückrufs definieren Sie den Typ, der zurückgegeben wird, nachdem die Argumente angegeben wurden.

  • Dieser Code setzt HideObjectMethods auf true, damit die IntelliSense-Erfahrung weiterhin optimiert bleibt. Dieses Attribut bewirkt, dass die Mitglieder Equals, GetHashCode, Finalize sowie GetType aus den IntelliSense-Listen für ein angegebenes Objekt unterdrückt werden.

  • Sie verwenden obj als Basistyp der Methode, aber Sie verwenden ein Regex Objekt als Laufzeitdarstellung dieses Typs, wie im nächsten Beispiel gezeigt.

  • Der Aufruf des Regex Konstruktors löst ein ArgumentException aus, wenn ein regulärer Ausdruck nicht gültig ist. Der Compiler fängt diese Ausnahme ab und meldet dem Benutzer zur Kompilierungszeit oder im Visual Studio-Editor eine Fehlermeldung. Mit dieser Ausnahme können reguläre Ausdrücke überprüft werden, ohne eine Anwendung auszuführen.

Der oben definierte Typ ist noch nicht hilfreich, da er keine sinnvollen Methoden oder Eigenschaften enthält. Fügen Sie zunächst eine statische IsMatch Methode hinzu:

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

Der vorherige Code definiert eine Methode IsMatch, die eine Zeichenfolge als Eingabe verwendet und ein bool zurückgibt. Der einzige schwierige Teil ist die Verwendung des args Arguments innerhalb der InvokeCode Definition. In diesem Beispiel ist args eine Liste von Zitaten, die die Argumente für diese Methode repräsentiert. Wenn es sich bei der Methode um eine Instanzmethode handelt, stellt das erste Argument das this Argument dar. Bei einer statischen Methode sind die Argumente jedoch nur die expliziten Argumente für die Methode. Beachten Sie, dass der Typ des zitierten Werts mit dem angegebenen Rückgabetyp übereinstimmen soll (in diesem Fall bool). Beachten Sie außerdem, dass dieser Code die AddXmlDoc Methode verwendet, um sicherzustellen, dass die bereitgestellte Methode auch über eine nützliche Dokumentation verfügt, die Sie über IntelliSense bereitstellen können.

Als Nächstes fügen Sie eine Instanzmethode 'Match' hinzu. Diese Methode muss jedoch einen Wert eines bereitgestellten Match-Typs zurückgeben, damit stark typisiert auf die Gruppen zugegriffen werden kann. Daher deklarieren Sie zuerst den Match Typ. Da dieser Typ von dem Muster abhängt, das als statisches Argument bereitgestellt wurde, muss dieser Typ in der parametrisierten Typdefinition geschachtelt werden:

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

ty.AddMember matchTy

Anschließend fügen Sie dem Match-Typ für jede Gruppe eine Eigenschaft hinzu. Zur Laufzeit wird eine Übereinstimmung als Match-Wert dargestellt, sodass das Zitat, das die Eigenschaft definiert, die indizierte Groups-Eigenschaft verwenden muss, um die entsprechende Gruppe abzurufen.

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

Beachten Sie erneut, dass Sie der bereitgestellten Eigenschaft XML-Dokumentation hinzufügen. Beachten Sie außerdem, dass eine Eigenschaft gelesen werden kann, wenn eine GetterCode Funktion bereitgestellt wird, und die Eigenschaft geschrieben werden kann, wenn eine SetterCode Funktion bereitgestellt wird, sodass die resultierende Eigenschaft nur lesbar ist.

Jetzt können Sie eine Instanzmethode erstellen, die einen Wert dieses Match Typs zurückgibt:

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

Da Sie eine Instanzmethode erstellen, stellt sie die args[0] Instanz dar, RegexTyped für die die Methode aufgerufen wird, und args[1] ist das Eingabeargument.

Stellen Sie schließlich einen Konstruktor bereit, damit Instanzen des bereitgestellten Typs erstellt werden können.

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

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

ty.AddMember ctor

Der Konstruktor führt bei der Löschung zur Erstellung einer einfachen .NET Regex-Standardinstanz, die wiederum in einem Objekt geschachtelt wird, da obj die Löschung des bereitgestellten Typs ist. Bei dieser Änderung funktioniert die Beispiel-API-Verwendung, die weiter oben im Thema angegeben wurde, wie erwartet. Der folgende Code ist vollständig und endgültig.

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

Wichtige Lektionen

In diesem Abschnitt wird erläutert, wie Sie einen Typanbieter erstellen, der mit seinen statischen Parametern arbeitet. Der Anbieter überprüft den statischen Parameter und stellt Vorgänge basierend auf seinem Wert bereit.

Ein Typanbieter, der lokale Daten verarbeitet

Häufig möchten Sie, dass Typanbieter APIs basierend auf nicht nur statischen Parametern, sondern auch Informationen aus lokalen oder Remotesystemen präsentieren. In diesem Abschnitt werden Typanbieter erläutert, die auf lokalen Daten basieren, z. B. lokale Datendateien.

Einfacher CSV-Dateianbieter

Betrachten Sie als einfaches Beispiel einen Typanbieter für den Zugriff auf wissenschaftliche Daten im CSV-Format (Comma Separated Value). In diesem Abschnitt wird davon ausgegangen, dass die CSV-Dateien eine Kopfzeile enthalten, gefolgt von Gleitkommadaten, wie die folgende Tabelle veranschaulicht:

Entfernung (Meter) Uhrzeit (Sekunde)
50.0 3.7
100.0 5.2
150.0 6.4

In diesem Abschnitt wird gezeigt, wie Sie einen Typ bereitstellen, mit dem Sie Zeilen mit einer Distance Eigenschaft vom Typ float<meter> und einer Time Eigenschaft vom Typ float<second>abrufen können. Aus Gründen der Einfachheit werden die folgenden Annahmen gemacht:

  • Kopfzeilennamen sind entweder ohne Einheit oder haben die Form "Name (Einheit)" und enthalten keine Kommas.

  • Einheiten sind alle „System International“ (SI)-Einheiten, wie sie das Modul FSharp.Data.UnitSystems.SI.UnitNames Module (F#) definiert.

  • Einheiten sind alle einfach (z. B. Meter) und nicht zusammengesetzt (z. B. Meter/Sekunde).

  • Alle Spalten enthalten Gleitkommadaten.

Ein vollständigerer Anbieter würde diese Einschränkungen lockern.

Der erste Schritt besteht darin, zu berücksichtigen, wie die API aussehen soll. Angesichts einer info.csv Datei mit dem Inhalt der vorherigen Tabelle (im durch Trennzeichen getrennten Format) sollten Benutzer des Anbieters Code schreiben können, der dem folgenden Beispiel ähnelt:

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

In diesem Fall sollte der Compiler diese Aufrufe in etwa wie im folgenden Beispiel konvertieren:

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

Für eine optimale Übersetzung muss der Typanbieter einen echten CsvFile-Typ in der Assembly des Typanbieters definieren. Typanbieter verlassen sich häufig auf einige Hilfstypen und Methoden, um wichtige Logik umzuschließen. Da die Maßeinheiten zur Laufzeit gelöscht werden, können Sie float[] als gelöschten Typ für eine Zeile verwenden. Der Compiler behandelt unterschiedliche Spalten als verschiedene Maßtypen. Beispielsweise hat die erste Spalte in unserem Beispiel Typ float<meter>, und die zweite hat float<second>. Die gelöschte Darstellung kann jedoch recht einfach bleiben.

Der folgende Code zeigt den Kern der Implementierung.

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

Beachten Sie die folgenden Punkte zur Implementierung:

  • Überladene Konstruktoren ermöglichen entweder die Originaldatei oder eine Datei, die ein identisches Schema enthält, zu lesen. Dieses Muster wird häufig verwendet, wenn Sie einen Typanbieter für lokale oder Remotedatenquellen schreiben. Mit diesem Muster kann eine lokale Datei als Vorlage für Remotedaten verwendet werden.

  • Sie können den TypeProviderConfig-Wert verwenden, der an den Typanbieterkonstruktor übergeben wird, um relative Dateinamen aufzulösen.

  • Mit der AddDefinitionLocation Methode können Sie den Speicherort der bereitgestellten Eigenschaften definieren. Wenn Sie daher für eine bereitgestellte Eigenschaft verwenden Go To Definition , wird die CSV-Datei in Visual Studio geöffnet.

  • Sie können den ProvidedMeasureBuilder Typ verwenden, um die SI-Einheiten nachzuschlagen und die relevanten float<_> Typen zu generieren.

Wichtige Lektionen

In diesem Abschnitt wird erläutert, wie Sie einen Typanbieter für eine lokale Datenquelle mit einem einfachen Schema erstellen, das in der Datenquelle selbst enthalten ist.

Weiter gehen

Die folgenden Abschnitte enthalten Vorschläge für weitere Studien.

Ein Blick auf den kompilierten Code für gelöschte Typen

Um Ihnen eine Vorstellung davon zu geben, wie die Verwendung des Typproviders dem ausgegebenen Code entspricht, schauen Sie sich mithilfe der weiter oben in diesem Thema verwendeten HelloWorldTypeProvider die folgende Funktion an.

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

Hier ist ein Bild des resultierenden Codes, der mithilfe von ildasm.exedekompiliert wird:

.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

Wie das Beispiel zeigt, wurden alle Erwähnungen des Typs Type1 und der InstanceProperty Eigenschaft gelöscht, sodass nur Vorgänge für die beteiligten Laufzeittypen verbleiben.

Design- und Benennungskonventionen für Typanbieter

Beachten Sie die folgenden Konventionen, wenn Sie Typanbieter erstellen.

Anbieter für Konnektivitätsprotokolle Im Allgemeinen sollten die Namen der meisten Anbieter-DLLs für Daten- und Dienstverbindungsprotokolle, z. B. OData- oder SQL-Verbindungen, auf TypeProvider oder TypeProviders enden. Verwenden Sie beispielsweise einen DLL-Namen, der der folgenden Zeichenfolge ähnelt:

Fabrikam.Management.BasicTypeProviders.dll

Stellen Sie sicher, dass Ihre bereitgestellten Typen Member des entsprechenden Namespaces sind, und geben Sie das verbindungsprotokoll an, das Sie implementiert haben:

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

Hilfsprogrammanbieter für allgemeine Codierung. Bei einem Hilfsprogrammtypanbieter, z. B. für reguläre Ausdrücke, kann der Typanbieter Teil einer Basisbibliothek sein, wie das folgende Beispiel zeigt:

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

In diesem Fall würde der bereitgestellte Typ an einem geeigneten Punkt gemäß den üblichen .NET-Entwurfskonventionen angezeigt:

  open Fabrikam.Core.Text.RegexTyped

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

Singleton-Datenquellen. Einige Typanbieter stellen eine Verbindung mit einer einzigen dedizierten Datenquelle her und stellen nur Daten bereit. In diesem Fall sollten Sie das TypeProvider Suffix ablegen und normale Konventionen für .NET-Benennung verwenden:

#r "Fabrikam.Data.Freebase.dll"

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

Weitere Informationen finden Sie in der GetConnection Designkonvention, die weiter unten in diesem Thema beschrieben wird.

Entwurfsmuster für Typanbieter

In den folgenden Abschnitten werden Entwurfsmuster beschrieben, die Sie beim Erstellen von Typanbietern heranziehen können.

Das GetConnection-Entwurfsmuster

Die meisten Typenanbieter sollten so geschrieben werden, dass sie das GetConnection-Muster verwenden, das von den Typanbietern in FSharp.Data.TypeProviders.dllverwendet wird, wie im folgenden Beispiel zu sehen ist.

#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

Typanbieter, die auf Remotedaten und -dienste zugreifen

Bevor Sie einen Typanbieter erstellen, der von Remotedaten und Diensten unterstützt wird, müssen Sie eine Reihe von Problemen berücksichtigen, die in der verbundenen Programmierung enthalten sind. Zu diesen Problemen gehören die folgenden Überlegungen:

  • Schemazuordnung

  • Aktivität und Ungültigkeit bei Schemaänderungen

  • Zwischenspeicherung von Schemas

  • asynchrone Implementierungen von Datenzugriffsvorgängen

  • Unterstützen von Abfragen, einschließlich LINQ-Abfragen

  • Anmeldeinformationen und Authentifizierung

In diesem Thema werden diese Probleme nicht weiter untersucht.

Weitere Techniken für Entwurf und Erstellung

Wenn Sie eigene Typanbieter schreiben, können die folgenden zusätzlichen Techniken ebenfalls hilfreich sein.

Erstellen von Typen und Mitgliedern nach Bedarf

Die ProvidedType-API hat versionen von AddMember verzögert.

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

Diese Versionen werden verwendet, um bei Bedarf Räume verschiedener Typen zu erstellen.

Bereitstellen von Arraytypen und generischen Typinstanziationen

Sie können bereitgestellte Member (deren Signaturen Arraytypen, byref-Typen und Instanziierungen von generischen Typen enthalten) erstellen, indem Sie die normalen Typen MakeArrayType, MakePointerType und MakeGenericType einer beliebigen Instanz von Type verwenden, einschließlich ProvidedTypeDefinitions.

Hinweis

In einigen Fällen müssen Sie möglicherweise den Helfer in ProvidedTypeBuilder.MakeGenericType verwenden. Weitere Informationen finden Sie in der Dokumentation zum Type Provider SDK .

Bereitstellung von Anmerkungen zu Maßeinheiten

Die ProvidedTypes-API stellt Hilfsprogramme zur Angabe von Maßeinheiten für Werte bereit. Verwenden Sie z. B. den folgenden Code, um den Typ float<kg>bereitzustellen:

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

Verwenden Sie den folgenden Code, um den Typ Nullable<decimal<kg/m^2>>bereitzustellen:

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

Zugreifen auf Project-Local- oder Script-Local ressourcen

Jede Instanz eines Typanbieters kann während der Konstruktion einen TypeProviderConfig Wert erhalten. Dieser Wert enthält den "Lösungsordner" für den Anbieter (d. h. den Projektordner für die Kompilierung oder das Verzeichnis, das ein Skript enthält), die Liste der assemblys, auf die verwiesen wird, und andere Informationen.

Ungültigmachung

Anbieter können ungültige Signale auslösen, um den F#-Sprachdienst zu benachrichtigen, dass sich die Schemaannahmen möglicherweise geändert haben. Wenn eine Ungültigkeit auftritt, wird eine Typüberprüfung erneut ausgeführt, wenn der Anbieter in Visual Studio gehostet wird. Dieses Signal wird ignoriert, wenn der Anbieter in F# Interactive oder vom F#-Compiler (fsc.exe) gehostet wird.

Zwischenspeichern von Schemainformationen

Anbieter müssen häufig den Zugriff auf Schemainformationen zwischenspeichern. Die zwischengespeicherten Daten sollten mithilfe eines Dateinamens gespeichert werden, der als statischer Parameter oder als Benutzerdaten angegeben wird. Ein Beispiel für die Schemazwischenspeicherung ist der LocalSchemaFile-Parameter in den Typanbietern in der FSharp.Data.TypeProviders-Assembly. Bei der Implementierung dieser Anbieter leitet dieser statische Parameter den Typanbieter an, die Schemainformationen in der angegebenen lokalen Datei anstelle des Zugriffs auf die Datenquelle über das Netzwerk zu verwenden. Um zwischengespeicherte Schemainformationen zu verwenden, müssen Sie auch den statischen Parameter ForceUpdate auf falsefestlegen. Sie können eine ähnliche Technik verwenden, um den Online- und Offlinedatenzugriff zu ermöglichen.

Unterstützungsassembly

Wenn Sie eine .dll-Datei oder .exe-Datei kompilieren, wird die zugehörige .dll-Datei für generierte Typen statisch in die Ergebnis-Assembly eingebunden. Dieser Link wird erstellt, indem die Typdefinitionen der Intermediate Language (IL) und alle verwalteten Ressourcen aus der Hintergrundassembly in die endgültige Assembly kopiert werden. Wenn Sie F# Interactive verwenden, wird die Hintergrund-.dll-Datei nicht kopiert, sondern stattdessen direkt in den F# Interactive-Prozess geladen.

Ausnahmen und Diagnose von Typanbietern

Jede Verwendung der Member von bereitgestellten Typen kann eine Ausnahme auslösen. Wenn ein Typanbieter in allen Fällen eine Ausnahme auslöst, attributet der Hostcompiler den Fehler einem bestimmten Typanbieter.

  • Typanbieterausnahmen sollten niemals zu internen Compilerfehlern führen.

  • Typanbieter können keine Warnungen ausgeben.

  • Wenn ein Typanbieter im F#-Compiler, in einer F#-Entwicklungsumgebung oder in F# Interactive gehostet wird, werden alle Ausnahmen von diesem Anbieter abgefangen. Die Message-Eigenschaft enthält dabei immer den Fehlertext, und es wird keine Stapelüberwachung angezeigt. Wenn Sie eine Ausnahme auslösen möchten, können Sie die folgenden Beispiele auslösen: System.NotSupportedException, , System.IO.IOException. System.Exception

Bereitstellung generierter Typen

Bisher wurde in diesem Dokument erläutert, wie gelöschte Typen bereitgestellt werden. Sie können auch den Typanbietermechanismus in F# verwenden, um generierte Typen bereitzustellen, die dem Programm des Benutzers als echte .NET-Typdefinitionen hinzugefügt werden. Sie müssen auf generierte bereitgestellte Typen verweisen, indem Sie eine Typdefinition verwenden.

open Microsoft.FSharp.TypeProviders

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

Der Hilfscode "ProvidedTypes-0.2", der Teil der F# 3.0-Version ist, bietet nur eingeschränkte Unterstützung für die Bereitstellung generierter Typen. Die folgenden Aussagen müssen für eine generierte Typdefinition wahr sein:

  • isErased muss auf false festgelegt werden.

  • Der generierte Typ muss einem neu erstellten ProvidedAssembly()Typ hinzugefügt werden, der einen Container für generierte Codefragmente darstellt.

  • Der Anbieter muss über eine Assembly verfügen, der eine tatsächliche .NET-DLL-Datei mit einer entsprechenden, auf dem Datenträger verfügbaren DLL-Datei zugrunde liegt.

Regeln und Einschränkungen

Berücksichtigen Sie beim Schreiben von Typanbietern die folgenden Regeln und Einschränkungen.

Bereitgestellte Typen müssen erreichbar sein

Alle bereitgestellten Typen müssen für die nicht geschachtelten Typen erreichbar sein. Die nicht geschachtelten Typen werden im Aufruf des TypeProviderForNamespaces-Konstruktors oder bei einem Aufruf von AddNamespace übergeben. Wenn der Anbieter z. B. den Typ StaticClass.P : T bereitstellt, müssen Sie sicherstellen, dass T entweder ein nicht geschachtelter Typ ist oder unter einem Typ geschachtelt wird.

Beispielsweise verfügen einige Anbieter über eine statische Klasse wie DataTypes, die diese T1, T2, T3, ... Typen enthalten. Andernfalls gibt der Fehler an, dass ein Verweis auf den Typ T in Assembly A gefunden wurde, aber der Typ konnte in dieser Assembly nicht gefunden werden. Wenn dieser Fehler angezeigt wird, stellen Sie sicher, dass alle Untertypen von den Anbietertypen erreicht werden können. Hinweis: Diese T1, T2, T3... Typen werden als " On-the-fly"- Typen bezeichnet. Denken Sie daran, diese in einen erreichbaren Namespace oder in einen übergeordneten Typ einzufügen.

Einschränkungen des Typanbietermechanismus

Der Typanbietermechanismus in F# hat die folgenden Einschränkungen:

  • Die zugrunde liegende Infrastruktur für Typanbieter in F# unterstützt keine bereitgestellten generischen Typen oder bereitgestellten generischen Methoden.

  • Der Mechanismus unterstützt keine geschachtelten Typen mit statischen Parametern.

Tipps zur Entwicklung

Möglicherweise finden Sie die folgenden Tipps während des Entwicklungsprozesses hilfreich:

Ausführen von zwei Instanzen von Visual Studio

Sie können den Typanbieter in einer Instanz entwickeln und den Anbieter in der anderen testen, da die Test-IDE eine Sperre für die .dll Datei erhält, die verhindert, dass der Typanbieter neu erstellt wird. Daher müssen Sie die zweite Instanz von Visual Studio schließen, während der Anbieter in der ersten Instanz integriert ist, und dann müssen Sie die zweite Instanz erneut öffnen, nachdem der Anbieter erstellt wurde.

Typanbieter durch Aufrufe von fsc.exe debuggen

Sie können Typanbieter mithilfe der folgenden Tools aufrufen:

  • fsc.exe (Der F#-Befehlszeilencompiler)

  • fsi.exe (Der F#-Interaktive Compiler)

  • devenv.exe (Visual Studio)

Sie können Typanbieter häufig am einfachsten debuggen, indem Sie fsc.exe in einer Testskriptdatei (z. B. script.fsx) verwenden. Sie können einen Debugger über eine Eingabeaufforderung starten.

devenv /debugexe fsc.exe script.fsx

Zur Protokollierung können Sie die normale Ausgabe auf die Standardausgabe verwenden.

Siehe auch