Esercitazione: Creare un provider di tipi

Il meccanismo del provider di tipi in F# è una parte significativa del supporto per la programmazione avanzata delle informazioni. Questa esercitazione illustra come creare provider di tipi personalizzati illustrando lo sviluppo di diversi provider di tipi semplici per illustrare i concetti di base. Per altre informazioni sul meccanismo del provider di tipi in F#, vedere Provider di tipi.

L'ecosistema F# contiene un intervallo di provider di tipi per i servizi dati Internet e aziendali comunemente usati. Ad esempio:

  • FSharp.Data include provider di tipi per formati di documento JSON, XML, CSV e HTML.

  • SwaggerProvider include due provider di tipi generativi che generano modelli a oggetti e client HTTP per le API descritte dagli schemi OpenApi 3.0 e Swagger 2.0.

  • FSharp.Data.SqlClient include un set di provider di tipi per l'incorporamento controllato in fase di compilazione di T-SQL in F#.

È possibile creare provider di tipi personalizzati oppure fare riferimento a provider di tipi creati da altri utenti. Ad esempio, l'organizzazione potrebbe avere un servizio dati che fornisce un numero elevato e crescente di set di dati denominati, ognuno con il proprio schema di dati stabile. È possibile creare un provider di tipi che legge gli schemi e presenta i set di dati correnti al programmatore in modo fortemente tipizzato.

Prima di iniziare

Il meccanismo del provider di tipi è progettato principalmente per l'inserimento di spazi stabili di dati e informazioni sul servizio nell'esperienza di programmazione F#.

Questo meccanismo non è progettato per l'inserimento di spazi informativi le cui modifiche allo schema durante l'esecuzione del programma sono rilevanti per la logica del programma. Inoltre, il meccanismo non è progettato per la meta-programmazione all'interno del linguaggio, anche se tale dominio contiene alcuni usi validi. È consigliabile usare questo meccanismo solo se necessario e dove lo sviluppo di un provider di tipi produce un valore molto elevato.

È consigliabile evitare di scrivere un provider di tipi in cui uno schema non è disponibile. Analogamente, è consigliabile evitare di scrivere un provider di tipi in cui sarebbe sufficiente una libreria .NET normale (o anche esistente).

Prima di iniziare, è possibile porre le domande seguenti:

  • Si dispone di uno schema per l'origine informazioni? In tal caso, qual è il mapping nel sistema di tipi F# e .NET?

  • È possibile usare un'API esistente (tipizzata in modo dinamico) come punto di partenza per l'implementazione?

  • L'utente e l'organizzazione avranno un numero sufficiente di usi del provider di tipi per renderlo utile per la scrittura? Una normale libreria .NET soddisfa le proprie esigenze?

  • Quanto cambierà lo schema?

  • Cambierà durante la codifica?

  • Cambierà tra le sessioni di codifica?

  • Cambierà durante l'esecuzione del programma?

I provider di tipi sono più adatti alle situazioni in cui lo schema è stabile in fase di esecuzione e durante la durata del codice compilato.

Provider di tipi semplici

Questo esempio è Samples.HelloWorldTypeProvider, simile agli esempi nella examples directory di F# Type Provider SDK. Il provider rende disponibile uno "spazio dei tipi" che contiene 100 tipi cancellati, come illustrato nel codice seguente usando la sintassi della firma F# e omettendo i dettagli per tutti tranne Type1. Per altre informazioni sui tipi cancellati, vedere Dettagli sui tipi specificati cancellati più avanti in questo argomento.

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

Si noti che il set di tipi e membri forniti è noto in modo statico. Questo esempio non sfrutta la capacità dei provider di fornire tipi che dipendono da uno schema. L'implementazione del provider di tipi è descritta nel codice seguente e i dettagli sono trattati nelle sezioni successive di questo argomento.

Avviso

Potrebbero esserci differenze tra questo codice e gli esempi online.

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

Per usare questo provider, aprire un'istanza separata di Visual Studio, creare uno script F# e quindi aggiungere un riferimento al provider dallo script usando #r come illustrato nel codice seguente:

#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

Cercare quindi i tipi nello Samples.HelloWorldTypeProvider spazio dei nomi generato dal provider di tipi.

Prima di ricompilare il provider, assicurarsi di aver chiuso tutte le istanze di Visual Studio e F# Interactive che usano la DLL del provider. In caso contrario, si verificherà un errore di compilazione perché la DLL di output verrà bloccata.

Per eseguire il debug di questo provider usando istruzioni print, creare uno script che espone un problema con il provider e quindi usare il codice seguente:

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

Per eseguire il debug di questo provider usando Visual Studio, aprire il prompt dei comandi per gli sviluppatori per Visual Studio con credenziali amministrative ed eseguire il comando seguente:

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

In alternativa, aprire Visual Studio, aprire il menu Debug, scegliere Debug/Attach to process…e collegarsi a un altro devenv processo in cui si sta modificando lo script. Usando questo metodo, è possibile specificare più facilmente la logica specifica nel provider di tipi digitando in modo interattivo le espressioni nella seconda istanza (con IntelliSense completo e altre funzionalità).

È possibile disabilitare il debug Just My Code per identificare meglio gli errori nel codice generato. Per informazioni su come abilitare o disabilitare questa funzionalità, vedere Esplorazione del codice con il debugger. Inoltre, è anche possibile impostare l'eccezione first-chance catching aprendo il Debug menu e quindi scegliendo Exceptions o scegliendo i tasti CTRL+ALT+E per aprire la Exceptions finestra di dialogo. In tale finestra di dialogo, in Common Language Runtime Exceptions, selezionare la Thrown casella di controllo.

Implementazione del provider di tipi

Questa sezione illustra le sezioni principali dell'implementazione del provider di tipi. Prima di tutto, si definisce il tipo per il provider di tipi personalizzato:

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

Questo tipo deve essere pubblico ed è necessario contrassegnarlo con l'attributo TypeProvider in modo che il compilatore riconosca il provider di tipi quando un progetto F# separato fa riferimento all'assembly che contiene il tipo. Il parametro config è facoltativo e, se presente, contiene informazioni di configurazione contestuali per l'istanza del provider di tipi creata dal compilatore F#.

Successivamente, si implementa l'interfaccia ITypeProvider . In questo caso, si usa il TypeProviderForNamespaces tipo dell'API ProvidedTypes come tipo di base. Questo tipo di helper può fornire una raccolta limitata di spazi dei nomi forniti con desiderio, ognuno dei quali contiene direttamente un numero finito di tipi fissi e forniti con entusiasmo. In questo contesto, il provider genera in modo eager i tipi anche se non sono necessari o usati.

inherit TypeProviderForNamespaces(config)

Definire quindi i valori privati locali che specificano lo spazio dei nomi per i tipi forniti e trovare l'assembly del provider di tipi stesso. Questo assembly viene usato in un secondo momento come tipo padre logico dei tipi cancellati forniti.

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

Creare quindi una funzione per fornire ognuno dei tipi Type1... Tipo100. Questa funzione viene illustrata in modo più dettagliato più avanti in questo argomento.

let makeOneProvidedType (n:int) = …

Generare quindi i 100 tipi forniti:

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

Aggiungere quindi i tipi come spazio dei nomi specificato:

do this.AddNamespace(namespaceName, types)

Aggiungere infine un attributo assembly che indica che si sta creando una DLL del provider di tipi:

[<assembly:TypeProviderAssembly>]
do()

Specifica di un tipo e dei relativi membri

La makeOneProvidedType funzione esegue il lavoro reale di fornire uno dei tipi .

let makeOneProvidedType (n:int) =
…

Questo passaggio illustra l'implementazione di questa funzione. Prima di tutto, creare il tipo specificato (ad esempio, Type1, quando n = 1 o Type57, quando 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>)

Si notino i punti seguenti:

  • Questo tipo specificato viene cancellato. Poiché si indica che il tipo di base è obj, le istanze verranno visualizzate come valori di tipo obj nel codice compilato.

  • Quando si specifica un tipo non annidato, è necessario specificare l'assembly e lo spazio dei nomi. Per i tipi cancellati, l'assembly deve essere l'assembly del provider di tipi stesso.

Aggiungere quindi la documentazione XML al tipo . Questa documentazione è ritardata, ovvero calcolata su richiesta se il compilatore host lo richiede.

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

Aggiungere quindi una proprietà statica fornita al tipo:

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

Ottenere questa proprietà restituirà sempre la stringa "Hello!". Per GetterCode la proprietà viene utilizzata un'offerta F#, che rappresenta il codice generato dal compilatore host per ottenere la proprietà . Per altre informazioni sulle offerte, vedere Virgolette di codice (F#).

Aggiungere la documentazione XML alla proprietà .

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

Associare ora la proprietà fornita al tipo specificato. È necessario associare un membro fornito a uno e a un solo tipo. In caso contrario, il membro non sarà mai accessibile.

t.AddMember staticProp

Creare ora un costruttore fornito che non accetta parametri.

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

Per InvokeCode il costruttore viene restituita un'offerta F#, che rappresenta il codice generato dal compilatore host quando viene chiamato il costruttore. Ad esempio, è possibile usare il costruttore seguente:

new Type10()

Verrà creata un'istanza del tipo fornito con i dati sottostanti "I dati oggetto". Il codice tra virgolette include una conversione in obj perché tale tipo è la cancellazione di questo tipo specificato (come specificato quando è stato dichiarato il tipo fornito).

Aggiungere la documentazione XML al costruttore e aggiungere il costruttore fornito al tipo specificato:

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

t.AddMember ctor

Creare un secondo costruttore fornito che accetta un parametro:

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

Per InvokeCode il costruttore restituisce di nuovo un'offerta F#, che rappresenta il codice generato dal compilatore host per una chiamata al metodo . Ad esempio, è possibile usare il costruttore seguente:

new Type10("ten")

Viene creata un'istanza del tipo fornito con i dati sottostanti "ten". È possibile che si sia già notato che la InvokeCode funzione restituisce un'offerta. L'input di questa funzione è un elenco di espressioni, una per parametro del costruttore. In questo caso, un'espressione che rappresenta il valore del singolo parametro è disponibile in args[0]. Il codice per una chiamata al costruttore collega il valore restituito al tipo objcancellato . Dopo aver aggiunto il secondo costruttore fornito al tipo, si crea una proprietà di istanza specificata:

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

Ottenere questa proprietà restituirà la lunghezza della stringa, ovvero l'oggetto rappresentazione. La GetterCode proprietà restituisce un'offerta F# che specifica il codice generato dal compilatore host per ottenere la proprietà . Come InvokeCode, la GetterCode funzione restituisce un'offerta. Il compilatore host chiama questa funzione con un elenco di argomenti. In questo caso, gli argomenti includono solo l'unica espressione che rappresenta l'istanza su cui viene chiamato il getter, a cui è possibile accedere usando args[0]. L'implementazione di GetterCode viene quindi inserita tra virgolette risultanti nel tipo objcancellato e viene usato un cast per soddisfare il meccanismo del compilatore per controllare i tipi che l'oggetto è una stringa. La parte successiva di fornisce un metodo di makeOneProvidedType istanza con un parametro.

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

Creare infine un tipo annidato contenente 100 proprietà annidate. La creazione di questo tipo annidato e le relative proprietà vengono ritardate, ovvero calcolate su richiesta.

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

Dettagli sui tipi specificati cancellati

L'esempio in questa sezione fornisce solo tipi specificati cancellati, particolarmente utili nelle situazioni seguenti:

  • Quando si scrive un provider per uno spazio informazioni che contiene solo dati e metodi.

  • Quando si scrive un provider in cui la semantica di tipo runtime accurata non è fondamentale per l'uso pratico dello spazio informativo.

  • Quando si scrive un provider per uno spazio di informazioni così grande e interconnesso che non è tecnicamente fattibile generare tipi .NET reali per lo spazio delle informazioni.

In questo esempio, ogni tipo fornito viene cancellato per il tipo obje tutti gli usi del tipo verranno visualizzati come tipo obj nel codice compilato. In effetti, gli oggetti sottostanti in questi esempi sono stringhe, ma il tipo verrà visualizzato come System.Object nel codice compilato .NET. Come per tutti gli usi di cancellazione dei tipi, è possibile usare boxing esplicito, unboxing e cast per i tipi cancellati. In questo caso, un'eccezione cast non valida può risultare quando viene usato l'oggetto . Un runtime del provider può definire il proprio tipo di rappresentazione privata per proteggere da false rappresentazioni. Non è possibile definire tipi cancellati in F#. È possibile cancellare solo i tipi specificati. È necessario comprendere le ramificazioni, sia pratiche che semantiche, dell'uso di tipi cancellati per il provider di tipi o di un provider che fornisce tipi cancellati. Un tipo cancellato non ha un tipo .NET reale. Pertanto, non è possibile eseguire una reflection accurata sul tipo e se si usano cast di runtime e altre tecniche che si basano sulla semantica esatta dei tipi di runtime. La subversione dei tipi cancellati comporta spesso eccezioni di cast dei tipi in fase di esecuzione.

Scelta delle rappresentazioni per i tipi specificati cancellati

Per alcuni usi di tipi specificati cancellati, non è necessaria alcuna rappresentazione. Ad esempio, il tipo specificato cancellato potrebbe contenere solo proprietà statiche e membri e nessun costruttore e nessun metodo o proprietà restituirà un'istanza del tipo. Se è possibile raggiungere istanze di un tipo specificato cancellato, è necessario considerare le domande seguenti:

Qual è la cancellazione di un tipo specificato?

  • La cancellazione di un tipo specificato è la modalità di visualizzazione del tipo nel codice .NET compilato.

  • La cancellazione di un tipo di classe cancellato fornito è sempre il primo tipo di base non cancellato nella catena di ereditarietà del tipo.

  • La cancellazione di un tipo di interfaccia cancellato fornito è sempre System.Object.

Quali sono le rappresentazioni di un tipo specificato?

  • Il set di oggetti possibili per un tipo specificato cancellato è denominato rappresentazioni. Nell'esempio di questo documento, le rappresentazioni di tutti i tipi Type1..Type100 specificati cancellati sono sempre oggetti stringa.

Tutte le rappresentazioni di un tipo specificato devono essere compatibili con la cancellazione del tipo specificato. In caso contrario, il compilatore F# restituirà un errore per l'uso del provider di tipi o il codice .NET non verificabile che non è valido verrà generato. Un provider di tipi non è valido se restituisce codice che fornisce una rappresentazione non valida.

È possibile scegliere una rappresentazione per gli oggetti forniti usando uno degli approcci seguenti, entrambi molto comuni:

  • Se si sta semplicemente fornendo un wrapper fortemente tipizzato su un tipo .NET esistente, spesso è opportuno cancellare il tipo in tale tipo, usare istanze di tale tipo come rappresentazioni o entrambe. Questo approccio è appropriato quando la maggior parte dei metodi esistenti su quel tipo ha ancora senso quando si usa la versione fortemente tipizzata.

  • Se si vuole creare un'API che differisce in modo significativo da qualsiasi API .NET esistente, è opportuno creare tipi di runtime che saranno la cancellazione dei tipi e le rappresentazioni per i tipi forniti.

Nell'esempio di questo documento vengono utilizzate stringhe come rappresentazioni di oggetti forniti. Spesso può essere opportuno usare altri oggetti per le rappresentazioni. Ad esempio, è possibile usare un dizionario come contenitore delle proprietà:

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

In alternativa, è possibile definire un tipo nel provider di tipi che verrà usato in fase di esecuzione per formare la rappresentazione, insieme a una o più operazioni di runtime:

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

I membri forniti possono quindi costruire istanze di questo tipo di oggetto:

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

In questo caso, è possibile usare (facoltativamente) questo tipo come cancellazione del tipo specificando questo tipo come quando baseType si costruisce :ProvidedTypeDefinition

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

Lezioni chiave

Nella sezione precedente è stato illustrato come creare un semplice provider di tipi di cancellazione che fornisce un intervallo di tipi, proprietà e metodi. In questa sezione è stato illustrato anche il concetto di cancellazione dei tipi, inclusi alcuni dei vantaggi e degli svantaggi della fornitura di tipi cancellati da un provider di tipi e sono state illustrate le rappresentazioni dei tipi cancellati.

Provider di tipi che usa parametri statici

La possibilità di parametrizzare i provider di tipi in base ai dati statici consente molti scenari interessanti, anche nei casi in cui il provider non deve accedere a dati locali o remoti. In questa sezione verranno illustrate alcune tecniche di base per mettere insieme un provider di questo tipo.

Provider regex controllato del tipo

Si supponga di voler implementare un provider di tipi per le espressioni regolari che esegue il wrapping delle librerie .NET Regex in un'interfaccia che fornisce le garanzie di compilazione seguenti:

  • Verifica della validità di un'espressione regolare.

  • Specifica di proprietà denominate sulle corrispondenze basate su qualsiasi nome di gruppo nell'espressione regolare.

In questa sezione viene illustrato come usare i provider di tipi per creare un RegexTyped tipo parametrizzato dal criterio di espressione regolare per offrire questi vantaggi. Il compilatore segnala un errore se il modello fornito non è valido e il provider di tipi può estrarre i gruppi dal modello in modo che sia possibile accedervi usando le proprietà denominate nelle corrispondenze. Quando si progetta un provider di tipi, è consigliabile considerare il modo in cui l'API esposta deve apparire agli utenti finali e come questa progettazione verrà convertita in codice .NET. L'esempio seguente illustra come usare tale API per ottenere i componenti del codice dell'area:

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"

Nell'esempio seguente viene illustrato come il provider di tipi converte queste chiamate:

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"

Notare i punti seguenti:

  • Il tipo Regex standard rappresenta il tipo con RegexTyped parametri.

  • Il RegexTyped costruttore genera una chiamata al costruttore Regex, passando l'argomento del tipo statico per il modello.

  • I risultati del Match metodo sono rappresentati dal tipo standard Match .

  • Ogni gruppo denominato restituisce una proprietà specificata e l'accesso alla proprietà comporta l'uso di un indicizzatore nella raccolta di Groups una corrispondenza.

Il codice seguente è il nucleo della logica per implementare tale provider e questo esempio omette l'aggiunta di tutti i membri al tipo specificato. Per informazioni su ogni membro aggiunto, vedere la sezione appropriata più avanti in questo argomento.

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

Notare i punti seguenti:

  • Il provider di tipi accetta due parametri statici: pattern, che è obbligatorio e options, che sono facoltativi (perché viene fornito un valore predefinito).

  • Dopo aver fornito gli argomenti statici, si crea un'istanza dell'espressione regolare. Questa istanza genererà un'eccezione se regex non è valido e questo errore verrà segnalato agli utenti.

  • All'interno del DefineStaticParameters callback si definisce il tipo che verrà restituito dopo l'invio degli argomenti.

  • Questo codice imposta HideObjectMethods su true in modo che l'esperienza IntelliSense rimanga semplificata. Questo attributo fa sì che i Equalsmembri , GetHashCodeFinalize, e GetType vengano eliminati dagli elenchi IntelliSense per un oggetto specificato.

  • Si usa obj come tipo di base del metodo, ma si userà un Regex oggetto come rappresentazione di runtime di questo tipo, come illustrato nell'esempio seguente.

  • La chiamata al Regex costruttore genera un'eccezione ArgumentException quando un'espressione regolare non è valida. Il compilatore rileva questa eccezione e segnala un messaggio di errore all'utente in fase di compilazione o nell'editor di Visual Studio. Questa eccezione consente di convalidare le espressioni regolari senza eseguire un'applicazione.

Il tipo definito in precedenza non è ancora utile perché non contiene metodi o proprietà significativi. Aggiungere prima di tutto un metodo statico IsMatch :

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

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

Il codice precedente definisce un metodo IsMatch, che accetta una stringa come input e restituisce un oggetto bool. L'unica parte complessa è l'uso dell'argomento args all'interno della InvokeCode definizione. In questo esempio è args un elenco di virgolette che rappresenta gli argomenti di questo metodo. Se il metodo è un metodo di istanza, il primo argomento rappresenta l'argomento this . Tuttavia, per un metodo statico, gli argomenti sono tutti solo gli argomenti espliciti per il metodo . Si noti che il tipo del valore tra virgolette deve corrispondere al tipo restituito specificato (in questo caso , bool). Si noti anche che questo codice usa il AddXmlDoc metodo per assicurarsi che il metodo fornito abbia anche una documentazione utile, che è possibile fornire tramite IntelliSense.

Aggiungere quindi un metodo Match di istanza. Tuttavia, questo metodo deve restituire un valore di un tipo fornito Match in modo che i gruppi possano essere accessibili in modo fortemente tipizzato. Pertanto, si dichiara prima di tutto il Match tipo. Poiché questo tipo dipende dal modello fornito come argomento statico, questo tipo deve essere annidato all'interno della definizione del tipo con parametri:

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

ty.AddMember matchTy

Si aggiunge quindi una proprietà al tipo Di corrispondenza per ogni gruppo. In fase di esecuzione, una corrispondenza viene rappresentata come Match valore, pertanto le virgolette che definiscono la proprietà devono utilizzare la Groups proprietà indicizzata per ottenere il gruppo pertinente.

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

Anche in questo caso, si noti che si aggiunge la documentazione XML alla proprietà specificata. Si noti anche che una proprietà può essere letta se viene fornita una GetterCode funzione e la proprietà può essere scritta se viene fornita una SetterCode funzione, quindi la proprietà risultante è di sola lettura.

È ora possibile creare un metodo di istanza che restituisce un valore di questo Match tipo:

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

Poiché si sta creando un metodo di istanza, args[0] rappresenta l'istanza RegexTyped in cui viene chiamato il metodo ed args[1] è l'argomento di input.

Specificare infine un costruttore in modo che sia possibile creare istanze del tipo fornito.

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

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

ty.AddMember ctor

Il costruttore cancella semplicemente la creazione di un'istanza regex .NET standard, che viene nuovamente sottoposto a boxing in un oggetto perché obj è la cancellazione del tipo specificato. Con questa modifica, l'utilizzo dell'API di esempio specificato in precedenza nell'argomento funziona come previsto. Il codice seguente è completo e finale:

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

Lezioni chiave

In questa sezione è stato illustrato come creare un provider di tipi che opera sui relativi parametri statici. Il provider controlla il parametro statico e fornisce operazioni in base al relativo valore.

Provider di tipi supportato da dati locali

Spesso è consigliabile che i provider di tipi presentino API in base non solo ai parametri statici, ma anche alle informazioni provenienti da sistemi locali o remoti. In questa sezione vengono illustrati i provider di tipi basati su dati locali, ad esempio i file di dati locali.

Provider di file CSV semplice

Come esempio semplice, si consideri un provider di tipi per l'accesso ai dati scientifici in formato CSV (Comma Separated Value). Questa sezione presuppone che i file CSV contengano una riga di intestazione seguita da dati a virgola mobile, come illustrato nella tabella seguente:

Distanza (metro) Ora (secondo)
50,0 3,7
100.0 5,2
150.0 6.4

Questa sezione illustra come specificare un tipo che è possibile usare per ottenere righe con una Distance proprietà di tipo e una Time proprietà di tipo float<second>float<meter> . Per semplicità, vengono effettuati i presupposti seguenti:

  • I nomi di intestazione sono senza unità o hanno il formato "Nome (unità)" e non contengono virgole.

  • Le unità sono tutte unità System International (SI) definite dal modulo FSharp.Data.UnitSystems.SI.UnitNames Module (F#).

  • Le unità sono tutte semplici (ad esempio, contatore) anziché composte (ad esempio, contatore/secondo).

  • Tutte le colonne contengono dati a virgola mobile.

Un provider più completo potrebbe allentare queste restrizioni.

Anche in questo caso, il primo passaggio consiste nel considerare l'aspetto dell'API. Dato un file info.csv con i contenuti della tabella precedente (nel formato delimitato da virgole), gli utenti del provider possono scrivere codice simile all'esempio seguente:

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

In questo caso, il compilatore deve convertire queste chiamate in un modo simile all'esempio seguente:

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

La conversione ottimale richiederà al provider di tipi di definire un tipo reale CsvFile nell'assembly del provider di tipi. I provider di tipi spesso si basano su alcuni tipi e metodi helper per eseguire il wrapping di una logica importante. Poiché le misure vengono cancellate in fase di esecuzione, è possibile usare come float[] tipo cancellato per una riga. Il compilatore considererà colonne diverse come tipi di misura diversi. Ad esempio, la prima colonna nell'esempio ha il tipo float<meter>e la seconda ha float<second>. Tuttavia, la rappresentazione cancellata può rimanere piuttosto semplice.

Il codice seguente illustra il nucleo dell'implementazione.

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

Si notino i punti seguenti sull'implementazione:

  • I costruttori di overload consentono di leggere il file originale o uno con uno schema identico. Questo modello è comune quando si scrive un provider di tipi per origini dati locali o remote e questo modello consente l'uso di un file locale come modello per i dati remoti.

  • È possibile usare il valore TypeProviderConfig passato al costruttore del provider di tipi per risolvere i nomi di file relativi.

  • È possibile utilizzare il AddDefinitionLocation metodo per definire la posizione delle proprietà specificate. Pertanto, se si usa Go To Definition in una proprietà specificata, il file CSV verrà aperto in Visual Studio.

  • È possibile usare il ProvidedMeasureBuilder tipo per cercare le unità SI e generare i tipi pertinenti float<_> .

Lezioni chiave

In questa sezione è stato illustrato come creare un provider di tipi per un'origine dati locale con uno schema semplice contenuto nell'origine dati stessa.

Approfondimenti

Le sezioni seguenti includono suggerimenti per ulteriori studi.

Esaminare il codice compilato per i tipi cancellati

Per dare un'idea del modo in cui l'uso del provider di tipi corrisponde al codice generato, esaminare la funzione seguente usando l'oggetto HelloWorldTypeProvider usato in precedenza in questo argomento.

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

Ecco un'immagine del codice risultante decompilato usando 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

Come illustrato nell'esempio, tutte le menzioni del tipo Type1 e la InstanceProperty proprietà sono state cancellate, lasciando coinvolte solo le operazioni sui tipi di runtime.

Progettazione e convenzioni di denominazione per i provider di tipi

Quando si creano provider di tipi, osservare le convenzioni seguenti.

I provider per i protocolli di Connessione ivity In generale, i nomi della maggior parte delle DLL del provider per i protocolli di connettività dei dati e dei servizi, ad esempio le connessioni OData o SQL, devono terminare in TypeProvider o TypeProviders. Ad esempio, usare un nome DLL simile alla stringa seguente:

Fabrikam.Management.BasicTypeProviders.dll

Assicurarsi che i tipi forniti siano membri dello spazio dei nomi corrispondente e indicare il protocollo di connettività implementato:

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

Provider di utilità per la codifica generale. Per un provider di tipi di utilità, ad esempio per le espressioni regolari, il provider di tipi può far parte di una libreria di base, come illustrato nell'esempio seguente:

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

In questo caso, il tipo fornito viene visualizzato in un punto appropriato in base alle normali convenzioni di progettazione .NET:

  open Fabrikam.Core.Text.RegexTyped

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

Origini dati Singleton. Alcuni provider di tipi si connettono a una singola origine dati dedicata e forniscono solo dati. In questo caso, è necessario eliminare il TypeProvider suffisso e usare le convenzioni normali per la denominazione .NET:

#r "Fabrikam.Data.Freebase.dll"

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

Per altre informazioni, vedere la GetConnection convenzione di progettazione descritta più avanti in questo argomento.

Modelli di progettazione per provider di tipi

Le sezioni seguenti descrivono i modelli di progettazione che è possibile usare per la creazione di provider di tipi.

Modello di progettazione Get Connessione ion

La maggior parte dei provider di tipi deve essere scritta per usare il GetConnection modello usato dai provider di tipi in FSharp.Data.TypeProviders.dll, come illustrato nell'esempio seguente:

#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

Provider di tipi supportati da dati e servizi remoti

Prima di creare un provider di tipi supportato da dati e servizi remoti, è necessario considerare una serie di problemi intrinseci nella programmazione connessa. Questi problemi includono le considerazioni seguenti:

  • mapping dello schema

  • liveness e invalidazione in presenza di modifiche dello schema

  • memorizzazione nella cache dello schema

  • implementazioni asincrone delle operazioni di accesso ai dati

  • supporto di query, incluse le query LINQ

  • credenziali e autenticazione

Questo argomento non esplora ulteriormente questi problemi.

Tecniche di creazione aggiuntive

Quando si scrivono provider di tipi personalizzati, è possibile usare le tecniche aggiuntive seguenti.

Creazione di tipi e membri su richiesta

L'API ProvidedType ha ritardato le versioni di AddMember.

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

Queste versioni vengono usate per creare spazi su richiesta di tipi.

Fornitura di tipi di matrice e istanze di tipi generici

I membri forniti (le cui firme includono tipi di matrice, tipi byref e istanze di tipi generici) usando il normale MakeArrayType, MakePointerTypee MakeGenericType in qualsiasi istanza di Type, incluso ProvidedTypeDefinitions.

Nota

In alcuni casi potrebbe essere necessario usare l'helper in ProvidedTypeBuilder.MakeGenericType. Per altri dettagli, vedere la documentazione di Type Provider SDK.

Aggiunta di annotazioni unità di misura

L'API ProvidedTypes fornisce helper per fornire annotazioni di misura. Ad esempio, per fornire il tipo float<kg>, usare il codice seguente:

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

Per specificare il tipo Nullable<decimal<kg/m^2>>, usare il codice seguente:

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

Accesso alle risorse locali del progetto o dello script

A ogni istanza di un provider di tipi può essere assegnato un TypeProviderConfig valore durante la costruzione. Questo valore contiene la "cartella di risoluzione" per il provider, ovvero la cartella del progetto per la compilazione o la directory che contiene uno script, l'elenco di assembly a cui si fa riferimento e altre informazioni.

Invalidazione

I provider possono generare segnali di invalidazione per notificare al servizio linguaggio F# che i presupposti dello schema potrebbero essere stati modificati. Quando si verifica l'invalidazione, un controllo del tipo viene eseguito di nuovo se il provider è ospitato in Visual Studio. Questo segnale verrà ignorato quando il provider è ospitato in F# Interactive o dal compilatore F# (fsc.exe).

Memorizzazione nella cache delle informazioni sullo schema

I provider devono spesso memorizzare nella cache l'accesso alle informazioni sullo schema. I dati memorizzati nella cache devono essere archiviati usando un nome file specificato come parametro statico o come dati utente. Un esempio di memorizzazione nella cache dello schema è il LocalSchemaFile parametro nei provider di tipi nell'assembly FSharp.Data.TypeProviders . Nell'implementazione di questi provider, questo parametro statico indirizza il provider di tipi a usare le informazioni sullo schema nel file locale specificato anziché accedere all'origine dati in rete. Per usare le informazioni sullo schema memorizzate nella cache, è necessario impostare anche il parametro ForceUpdate statico su false. È possibile usare una tecnica simile per abilitare l'accesso ai dati online e offline.

Assembly di backup

Quando si compila un .dll file o .exe , il file di backup .dll per i tipi generati viene collegato in modo statico all'assembly risultante. Questo collegamento viene creato copiando le definizioni dei tipi del linguaggio intermedio (IL) e tutte le risorse gestite dall'assembly di backup nell'assembly finale. Quando si usa F# Interactive, il file di backup .dll non viene copiato e viene invece caricato direttamente nel processo F# Interactive.

Eccezioni e diagnostica da provider di tipi

Tutti gli usi di tutti i membri di tipi forniti possono generare eccezioni. In tutti i casi, se un provider di tipi genera un'eccezione, il compilatore host assegna l'errore a un provider di tipi specifico.

  • Le eccezioni del provider di tipi non devono mai generare errori interni del compilatore.

  • I provider di tipi non possono segnalare avvisi.

  • Quando un provider di tipi è ospitato nel compilatore F#, un ambiente di sviluppo F# o F# Interactive, vengono rilevate tutte le eccezioni di tale provider. La proprietà Message è sempre il testo dell'errore e non viene visualizzata alcuna traccia dello stack. Se si intende generare un'eccezione, è possibile generare gli esempi seguenti: System.NotSupportedException, System.IO.IOException, System.Exception.

Fornitura di tipi generati

Finora, questo documento ha spiegato come fornire tipi cancellati. È anche possibile usare il meccanismo del provider di tipi in F# per fornire tipi generati, che vengono aggiunti come definizioni di tipi .NET reali nel programma degli utenti. È necessario fare riferimento ai tipi forniti generati usando una definizione di tipo.

open Microsoft.FSharp.TypeProviders

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

Il codice helper ProvidedTypes-0.2 che fa parte della versione F# 3.0 ha solo un supporto limitato per fornire tipi generati. Le istruzioni seguenti devono essere vere per una definizione di tipo generato:

  • isErased deve essere impostato su false.

  • Il tipo generato deve essere aggiunto a un oggetto appena costruito ProvidedAssembly(), che rappresenta un contenitore per frammenti di codice generati.

  • Il provider deve avere un assembly con un file .NET di backup effettivo .dll con un file .dll corrispondente su disco.

Regole e limitazioni

Quando si scrivono provider di tipi, tenere presenti le regole e le limitazioni seguenti.

I tipi forniti devono essere raggiungibili

Tutti i tipi forniti devono essere raggiungibili dai tipi non annidati. I tipi non annidati vengono specificati nella chiamata al TypeProviderForNamespaces costruttore o a una chiamata a AddNamespace. Ad esempio, se il provider fornisce un tipo StaticClass.P : T, è necessario assicurarsi che T sia un tipo non annidato o annidato in uno.

Ad esempio, alcuni provider hanno una classe statica, DataTypes ad esempio che contiene questi T1, T2, T3, ... tipi. In caso contrario, l'errore indica che è stato trovato un riferimento al tipo T nell'assembly A, ma non è stato possibile trovare il tipo in tale assembly. Se viene visualizzato questo errore, verificare che tutti i sottotipi possano essere raggiunti dai tipi di provider. Nota: questi T1, T2, T3... tipi sono definiti tipi on-the-fly . Ricordarsi di inserirli in uno spazio dei nomi accessibile o in un tipo padre.

Limitazioni del meccanismo del provider di tipi

Il meccanismo del provider di tipi in F# presenta le limitazioni seguenti:

  • L'infrastruttura sottostante per i provider di tipi in F# non supporta i tipi generici forniti o i metodi generici forniti.

  • Il meccanismo non supporta i tipi annidati con parametri statici.

Suggerimenti per lo sviluppo

Durante il processo di sviluppo potrebbero essere utili i suggerimenti seguenti:

Eseguire due istanze di Visual Studio

È possibile sviluppare il provider di tipi in un'istanza e testare il provider nell'altro perché l'IDE di test eseguirà un blocco sul file .dll che impedisce la ricompilazione del provider di tipi. Pertanto, è necessario chiudere la seconda istanza di Visual Studio mentre il provider viene compilato nella prima istanza e quindi è necessario riaprire la seconda istanza dopo la compilazione del provider.

Eseguire il debug dei provider di tipi usando chiamate di fsc.exe

È possibile richiamare provider di tipi usando gli strumenti seguenti:

  • fsc.exe (compilatore della riga di comando F#)

  • fsi.exe (compilatore F# Interactive)

  • devenv.exe (Visual Studio)

È spesso possibile eseguire il debug dei provider di tipi più facilmente usando fsc.exe in un file di script di test, ad esempio script.fsx. È possibile avviare un debugger da un prompt dei comandi.

devenv /debugexe fsc.exe script.fsx

È possibile usare la registrazione da stampa a stdout.

Vedi anche