Compartir a través de


Tutorial: Creación de un proveedor de tipos

El mecanismo del proveedor de tipos en F# es una parte significativa de su compatibilidad con la programación enriquecida de información. En este tutorial se explica cómo crear sus propios proveedores de tipos mediante el desarrollo de varios proveedores de tipos simples para ilustrar los conceptos básicos. Para obtener más información sobre el mecanismo de proveedor de tipos en F#, vea Proveedores de tipos.

El ecosistema de F# contiene una variedad de proveedores de tipos para los servicios de datos de Internet y de empresa usados habitualmente. Por ejemplo:

  • FSharp.Data incluye proveedores de tipos para formatos de documento JSON, XML, CSV y HTML.

  • SwaggerProvider incluye dos proveedores de tipos generativos que generan el modelo de objetos y los clientes HTTP para las API descritas por los esquemas de OpenApi 3.0 y Swagger 2.0.

  • FSharp.Data.SqlClient tiene un conjunto de proveedores de tipos de datos que permiten la comprobación en tiempo de compilación y la inserción de T-SQL en F#.

Puede crear proveedores de tipos personalizados o puede hacer referencia a proveedores de tipos creados por otros usuarios. Por ejemplo, su organización podría tener un servicio de datos que proporcione un gran y creciente número de conjuntos de datos con nombre, cada uno con su propio esquema de datos estable. Se puede crear un proveedor de tipos que lea los esquemas y presente los conjuntos de datos actuales al programador de una manera fuertemente tipada.

Antes de empezar

El mecanismo de proveedor de tipos está diseñado principalmente para insertar espacios estables de información de datos y servicios en la experiencia de programación de F#.

Este mecanismo no está diseñado para insertar espacios de información cuyos cambios de esquema durante la ejecución del programa sean relevantes para la lógica del programa. Además, el mecanismo no está diseñado para la meta-programación dentro del lenguaje, aunque ese dominio contenga algunos usos válidos. Debería usar este mecanismo solo cuando sea necesario y cuando el desarrollo de un proveedor de tipos de datos produzca un valor muy alto.

Debe evitar escribir un proveedor de tipos cuando no hay un esquema disponible. Del mismo modo, debe evitar crear un proveedor de tipos cuando una biblioteca .NET, ya sea normal o existente, sea suficiente.

Antes de empezar, puede hacer las siguientes preguntas:

  • ¿Tiene un esquema para el origen de información? Si lo tiene, ¿cuál es la correspondencia entre los sistemas de tipos de F# y .NET?

  • ¿Puede usar una API existente (con tipo dinámico) como punto de partida para la implementación?

  • ¿Tendrá usted y su organización suficientes usos del proveedor de tipos para que la escritura valga la pena? ¿Una biblioteca de .NET normal satisface sus necesidades?

  • ¿Cuánto cambiará su esquema?

  • ¿Cambiará durante la codificación?

  • ¿Cambiará entre sesiones de codificación?

  • ¿Cambiará durante la ejecución del programa?

Los proveedores de tipos son más adecuados para situaciones en las que el esquema es estable en tiempo de ejecución y durante la vigencia del código compilado.

Un proveedor de tipos simple

Este ejemplo es Samples.HelloWorldTypeProvider, similar a los ejemplos del examples directorio del SDK del proveedor de tipos de F#. El proveedor pone a disposición un "espacio de tipos" que contiene 100 tipos eliminados, como lo muestra el siguiente código utilizando la sintaxis de firma de F# y omitiendo los detalles de todos, excepto Type1. Para obtener más información sobre los tipos borrados, vea Detalles sobre los tipos proporcionados borrados más adelante en este tema.

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

Tenga en cuenta que el conjunto de tipos y miembros proporcionados se conoce estáticamente. En este ejemplo no se aprovecha la capacidad de los proveedores de proporcionar tipos que dependen de un esquema. La implementación del proveedor de tipos se describe en el código siguiente y los detalles se tratan en secciones posteriores de este tema.

Advertencia

Puede haber diferencias entre este código y los ejemplos en línea.

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

Para usar este proveedor, abra una instancia independiente de Visual Studio, cree un script de F# y agregue una referencia al proveedor desde el script mediante #r como se muestra en el código siguiente:

#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

A continuación, busque los tipos en el Samples.HelloWorldTypeProvider espacio de nombres que generó el proveedor de tipos.

Antes de volver a compilar el proveedor, asegúrese de que ha cerrado todas las instancias de Visual Studio y F# Interactive que usan el archivo DLL del proveedor. De lo contrario, se producirá un error de compilación porque el archivo DLL de salida se bloqueará.

Para depurar este proveedor mediante instrucciones de impresión, cree un script que exponga un problema con el proveedor y, a continuación, utilice el código siguiente:

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

Para depurar este proveedor mediante Visual Studio, abra el símbolo del sistema de Visual Studio con credenciales administrativas y ejecute el comando siguiente:

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

Como alternativa, abra Visual Studio, abra el menú Depurar, elija Debug/Attach to process… y adjúntese a otro proceso devenv donde estás editando tu script. Con este método, puede dirigirse más fácilmente a una lógica concreta en el proveedor de tipos escribiendo de forma interactiva expresiones en la segunda instancia (con IntelliSense completo y otras características).

Puede deshabilitar la depuración Just My Code para identificar mejor los errores en el código generado. Para obtener información sobre cómo habilitar o deshabilitar esta característica, vea Navegar por el código con el depurador. Además, también puede establecer la detección de excepciones de primera oportunidad abriendo el Debug menú y seleccionando Exceptions o seleccionando las teclas Ctrl+Alt+E para abrir el Exceptions cuadro de diálogo. En ese cuadro de diálogo, en Common Language Runtime Exceptions, seleccione la casilla Thrown.

Implementación del proveedor de tipos

En esta sección se muestran las etapas principales de la implementación del proveedor de tipos. En primer lugar, defina el tipo para el propio proveedor de tipos personalizados:

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

Este tipo debe ser público y debe marcarlo con el atributo TypeProvider para que el compilador reconozca el proveedor de tipos cuando un proyecto de F# independiente haga referencia al ensamblado que contiene el tipo. El parámetro config es opcional y, si está presente, contiene información de configuración contextual para la instancia del proveedor de tipos que crea el compilador de F#.

A continuación, implementará la interfaz ITypeProvider . En este caso, se usa el TypeProviderForNamespaces tipo de la ProvidedTypes API como un tipo base. Este tipo del asistente puede proporcionar una colección finita de espacios de nombres proporcionados anticipadamente, cada uno de los cuales contiene directamente un número finito de tipos fijos proporcionados anticipadamente. En este contexto, el proveedor genera diligentemente tipos incluso si no son necesarios o se usan.

inherit TypeProviderForNamespaces(config)

A continuación, defina valores privados locales que especifiquen el espacio de nombres para los tipos proporcionados y busque el propio ensamblado del proveedor de tipos. Este ensamblaje se usa más adelante como el tipo lógico principal de los tipos eliminados que se proporcionan.

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

A continuación, cree una función para proporcionar cada uno de los tipos Type1... Tipo100. Esta función se explica con más detalle más adelante en este tema.

let makeOneProvidedType (n:int) = …

A continuación, genere los 100 tipos proporcionados:

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

Después, agregue los tipos como un espacio de nombres proporcionado:

do this.AddNamespace(namespaceName, types)

Finalmente, agregue un atributo de ensamblado que indique que está creando una DLL de proveedor de tipos:

[<assembly:TypeProviderAssembly>]
do()

Proporcionar un tipo y sus miembros

La makeOneProvidedType función realiza el trabajo real de proporcionar uno de los tipos.

let makeOneProvidedType (n:int) =
…

En este paso se explica la implementación de esta función. En primer lugar, cree el tipo proporcionado (por ejemplo, Type1, cuando n = 1 o Type57, cuando 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>)

Debe tener en cuenta los siguientes puntos:

  • Este tipo proporcionado se borra. Dado que indica que el tipo base es obj, las instancias aparecerán como valores de tipo obj en código compilado.

  • Cuando especifique un tipo no anidado, deberá especificar también el ensamblado y el espacio de nombres. Para los tipos borrados, el ensamblado deberá ser el propio ensamblado del proveedor de tipos.

A continuación, agregue documentación XML al tipo . Esta documentación se retrasa, es decir, se calcula a petición si el compilador del host lo necesita.

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

A continuación, agregue una propiedad estática proporcionada al tipo :

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

Al obtener esta propiedad, siempre se evaluará como la cadena "Hello!". La función GetterCode de la propiedad utiliza una expresión de código delimitada de F# que representa el código que genera el compilador host para obtener la propiedad. Para obtener más información sobre las citas, consulte Citas de código (F#).

Agregue documentación XML a la propiedad .

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

Ahora asocie la propiedad proporcionada al tipo proporcionado. Debe asociar un miembro proporcionado a un único tipo. De lo contrario, el miembro nunca será accesible.

t.AddMember staticProp

Ahora cree un constructor proporcionado que no tenga parámetros.

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

El InvokeCode del constructor devuelve una cita de F#, que representa el código que genera el compilador anfitrión cuando se invoca el constructor. Por ejemplo, puede usar el constructor siguiente:

new Type10()

Se creará una instancia del tipo proporcionado con los datos subyacentes "Los datos del objeto". El código entrecomillado incluye una conversión a obj porque ese tipo es la eliminación de este tipo proporcionado (como especificó al declarar el tipo proporcionado).

Agregue documentación XML al constructor y agregue el constructor proporcionado al tipo proporcionado:

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

t.AddMember ctor

Cree un segundo constructor proporcionado que tome un parámetro:

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

La función InvokeCode del constructor devuelve de nuevo una expresión de código delimitada de F# que representa el código que el compilador host generó para una llamada al método. Por ejemplo, puede usar el constructor siguiente:

new Type10("ten")

Se crea una instancia del tipo proporcionado con los datos subyacentes "ten". Es posible que ya haya observado que la función InvokeCode devuelve una cotización. La entrada a esta función es una lista de expresiones, una por parámetro de constructor. En este caso, hay disponible una expresión que representa el valor de parámetro único en args[0]. El código de una llamada al constructor convierte el valor devuelto en el tipo borrado obj. Después de agregar el segundo constructor proporcionado al tipo, cree una propiedad de instancia proporcionada:

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

Al obtener esta propiedad se devolverá la longitud de la cadena, que es el objeto de representación. La GetterCode propiedad devuelve una cita de F# que especifica el código que genera el compilador anfitrión para acceder a la propiedad. Al igual que InvokeCode, la función GetterCode devuelve una cotización. El compilador host llama a esta función con una lista de argumentos. En este caso, los argumentos incluyen solo la expresión única que representa la instancia en la que se llama al captador, a la que se puede acceder mediante args[0]. Seguidamente, la implementación de GetterCode se une a la expresión de código delimitada resultante en el tipo borrado obj y se utiliza una conversión para satisfacer el mecanismo del compilador que comprueba los tipos así como que el objeto es una cadena. La siguiente parte de makeOneProvidedType proporciona un método de instancia con un parámetro.

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

Por último, cree un tipo anidado que contenga 100 propiedades anidadas. La creación de este tipo anidado y sus propiedades se demora, es decir, se calcula a petición.

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

Detalles sobre los tipos proporcionados borrados

El ejemplo de esta sección solo proporciona tipos proporcionados borrados, que son especialmente útiles en las situaciones siguientes:

  • Cuando se escribe un proveedor para un espacio de información que solo contiene datos y métodos.

  • Cuando se escribe un proveedor en el que la semántica precisa de tipos en tiempo de ejecución no es fundamental para el uso práctico del espacio de información.

  • Al escribir un proveedor para un espacio de información tan vasto y entrelazado que no es técnicamente factible generar tipos de .NET reales para dicho espacio.

En este ejemplo, cada tipo proporcionado se borra al tipo objy todos los usos del tipo aparecerán como tipo obj en código compilado. De hecho, los objetos subyacentes de estos ejemplos son cadenas, pero el tipo aparecerá como System.Object en el código compilado de .NET. Como con todos los usos del borrado de tipos, se puede utilizar la conversión boxing explícita, la conversión unboxing y la conversión para trastocar los tipos borrados. En este caso, puede producirse una excepción de conversión no válida cuando se utiliza el objeto. Un entorno de ejecución de proveedor puede definir su propio tipo de representación privada para ayudar a protegerse frente a representaciones falsas. No se pueden definir tipos borrados en el propio F#. Solo se pueden borrar los tipos proporcionados. Se deben entender las implicaciones, tanto prácticas como semánticas, de utilizar los tipos borrados para el proveedor de tipo o un proveedor que proporcione tipos borrados. Un tipo eliminado no tiene un tipo de .NET propiamente dicho. Por consiguiente, no se puede hacer una reflexión precisa sobre el tipo y se podrían trastocar los tipos borrados si se utilizan conversiones en tiempo de ejecución y otras técnicas que dependen de semánticas exactas de tipos en tiempo de ejecución. La subversión de tipos borrados frecuentemente da lugar a excepciones de conversión de tipos en tiempo de ejecución.

Elegir representaciones para los tipos proporcionados borrados

Para algunos usos de los tipos proporcionados borrados, no se requiere ninguna representación. Por ejemplo, el tipo proporcionado borrado podría contener solo propiedades estáticas y miembros y ningún constructor, y ningún método o propiedades devolvería una instancia del tipo. Si puede tener acceso a instancias de un tipo proporcionado borrado, considere las preguntas siguientes:

¿Qué es la eliminación de un tipo dado?

  • La eliminación de un tipo proporcionado es cómo aparece el tipo en el código .NET compilado.

  • El borrado de una clase de un tipo proporcionado borrado siempre es el primer tipo base no borrado de la cadena de herencia del tipo.

  • El borrado de una interfaz de un tipo de borrado proporcionado es siempre System.Object.

¿Cuáles son las representaciones de un tipo proporcionado?

  • El conjunto de objetos posibles para un tipo borrado proporcionado se denomina sus representaciones. En el ejemplo de este documento, las representaciones de todos los tipos Type1..Type100 proporcionados borrados siempre son objetos de cadena.

Todas las representaciones de un tipo proporcionado deben ser compatibles con la eliminación del tipo proporcionado. (De lo contrario, el compilador de F# proporcionará un error para un uso del proveedor de tipos o se generará un código de .NET no verificable que no sea válido. Un proveedor de tipos no es válido si devuelve código que proporciona una representación que no es válida).

Puede elegir una representación para los objetos proporcionados mediante cualquiera de los métodos siguientes, ambos son muy comunes:

  • Si lo que se hace es simplemente proporcionar un contenedor fuertemente tipado sobre un tipo existente de .NET, tiene sentido que el tipo borre ese tipo, use instancias de ese tipo como representaciones, o ambas cosas. Este enfoque es adecuado cuando la mayoría de los métodos existentes de ese tipo siguen teniendo sentido al emplear la versión con tipado fuerte.

  • Si lo que se desea es crear una API que difiera significativamente de cualquier API existente de .NET, tiene sentido crear tipos en tiempo de ejecución que constituyan la representación y el borrado de tipos para los tipos proporcionados.

En el ejemplo de este documento se usan cadenas como representaciones de objetos proporcionados. Con frecuencia, puede ser adecuado usar otros objetos para representaciones. Por ejemplo, se puede utilizar un diccionario como contenedor de propiedades:

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

Como alternativa, puede definir un tipo en el proveedor de tipos que se usará en tiempo de ejecución para formar la representación, junto con una o varias operaciones en tiempo de ejecución:

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

A continuación, los miembros proporcionados pueden construir instancias de este tipo de objeto:

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

En este caso, puede (opcionalmente) usar este tipo como borrado de tipos al especificar este tipo como baseType al construir el ProvidedTypeDefinition.

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

Lecciones clave

En la sección anterior se explica cómo crear un proveedor de tipos de borrado sencillo que proporciona una variedad de tipos, propiedades y métodos. En esta sección también se explicó el concepto de borrado de tipos, incluidas algunas de las ventajas y desventajas de proporcionar tipos borrados de un proveedor de tipos y se describen las representaciones de tipos borrados.

Un proveedor de tipos que utiliza parámetros estáticos

La capacidad de parametrizar proveedores de tipos por datos estáticos permite muchos escenarios interesantes, incluso en casos en los que el proveedor no necesita acceder a ningún dato local o remoto. En esta sección, aprenderá algunas técnicas básicas para crear un proveedor así.

Proveedor de tipo de comprobación de expresiones regulares

Imagine que desea implementar un proveedor de tipos para expresiones regulares que encapsula las bibliotecas de .NET Regex en una interfaz que proporciona las siguientes garantías en tiempo de compilación:

  • Comprobar si una expresión regular es válida.

  • Proporcionar propiedades con nombre en las coincidencias basadas en cualquier nombre de grupo de la expresión regular.

En esta sección se muestra cómo usar proveedores de tipos para crear un RegexTyped tipo que parametrice el patrón de expresión regular para proporcionar estas ventajas. El compilador notificará un error si el patrón proporcionado no es válido y el proveedor de tipos puede extraer los grupos del patrón para que pueda acceder a ellos mediante propiedades con nombre en coincidencias. Al diseñar un proveedor de tipos, debe tener en cuenta la apariencia de su API expuesta a los usuarios finales y cómo se traducirá este diseño en código .NET. En el ejemplo siguiente se muestra cómo usar dicha API para obtener los componentes del código de área:

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"

En el ejemplo siguiente se muestra cómo el proveedor de tipos traduce estas llamadas:

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"

Tenga en cuenta los siguientes puntos:

  • El tipo Regex estándar representa el tipo parametrizado RegexTyped.

  • El RegexTyped constructor da como resultado una llamada al constructor Regex, pasando el argumento de tipo estático para el patrón.

  • Los resultados del Match método se representan mediante el tipo estándar Match .

  • Cada grupo con nombre produce una propiedad proporcionada, y el acceso a la propiedad produce el uso de un indexador en la colección Groups de una coincidencia.

El código siguiente es el núcleo de la lógica para implementar este proveedor y en este ejemplo se omite la adición de todos los miembros al tipo proporcionado. Para obtener información sobre cada miembro agregado, consulte la sección adecuada más adelante en este tema.

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

Tenga en cuenta los siguientes puntos:

  • El proveedor de tipos toma dos parámetros estáticos: pattern, que es obligatorio y options, que son opcionales (porque se proporciona un valor predeterminado).

  • Una vez proporcionados los argumentos estáticos, se crea una instancia de la expresión regular. Esta instancia producirá una excepción si regex tiene un formato incorrecto y este error se notificará a los usuarios.

  • Dentro del DefineStaticParameters callback, se define el tipo que se devolverá después de que se proporcionen los argumentos.

  • Este código establece HideObjectMethods en verdadero para que la experiencia de IntelliSense se mantenga simplificada. Este atributo hace que los Equalsmiembros , GetHashCode, Finalizey GetType se supriman de las listas de IntelliSense para un objeto proporcionado.

  • Se usa obj como tipo base del método, pero usará un Regex objeto como representación en tiempo de ejecución de este tipo, como se muestra en el ejemplo siguiente.

  • La llamada al constructor Regex inicia una excepción ArgumentException cuando una expresión regular no es válida. El compilador detecta esta excepción e informa de un mensaje de error al usuario en tiempo de compilación o en el editor de Visual Studio. Esta excepción permite validar expresiones regulares sin ejecutar una aplicación.

El tipo definido anteriormente no es útil aún porque no contiene ningún método o propiedades significativos. En primer lugar, agregue un método estático 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

El código anterior define un método IsMatch, que toma una cadena como entrada y devuelve un bool. La única parte complicada es el uso del argumento args dentro de la definición InvokeCode. En este ejemplo, args es una lista de citas que representa los argumentos de este método. Si el método es un método de instancia, el primer argumento representa el this argumento . Sin embargo, para un método estático, los argumentos son solo los argumentos explícitos para el método . Tenga en cuenta que el tipo del valor entrecomillado debe coincidir con el tipo de valor devuelto especificado (en este caso, bool). Tenga en cuenta también que este código usa el AddXmlDoc método para asegurarse de que el método proporcionado también tiene documentación útil, que puede proporcionar a través de IntelliSense.

A continuación, agregue un método de instancia Match. Sin embargo, este método debe devolver un valor de un tipo Match proporcionado, de modo que se pueda tener acceso a los grupos de manera fuertemente tipada. Por ello, primero se declara el tipo Match. Dado que este tipo depende del patrón que se proporcionó como argumento estático, este tipo debe anidarse dentro de la definición de tipo con parámetros:

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

ty.AddMember matchTy

A continuación, se agrega una propiedad al tipo Match de cada grupo. En tiempo de ejecución, una coincidencia se representa como un Match valor, por lo que la cita que define la propiedad debe usar la Groups propiedad indexada para obtener el grupo correspondiente.

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

Una vez más, observe que se está agregando la documentación XML a la propiedad proporcionada. Tenga en cuenta también que una propiedad se puede leer si se proporciona una GetterCode función y la propiedad se puede escribir si se proporciona una SetterCode función, por lo que la propiedad resultante es de solo lectura.

Ahora puede crear un método de instancia que devuelva un valor de este 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

Dado que va a crear un método de instancia, args[0] representa la RegexTyped instancia en la que se llama al método y args[1] es el argumento de entrada.

Por último, proporcione un constructor para que se puedan crear instancias del tipo proporcionado.

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

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

ty.AddMember ctor

El constructor simplemente borra para la creación de una Regex estándar de .NET, a la cual también se le aplica la conversión box en un objeto porque obj es el borrado del tipo proporcionado. Con ese cambio, el uso de la API de ejemplo que especificó anteriormente en el tema funciona según lo previsto. El siguiente código está completo y definitivo.

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

Lecciones clave

En esta sección se explica cómo crear un proveedor de tipos que funcione en sus parámetros estáticos. El proveedor comprueba el parámetro estático y proporciona operaciones basadas en su valor.

Un proveedor de tipos que está respaldado por datos locales

Con frecuencia, es posible que desee que los proveedores de tipos presenten API basadas en no solo parámetros estáticos, sino también en información de sistemas locales o remotos. En esta sección se describen los proveedores de tipos basados en datos locales, como archivos de datos locales.

Proveedor de archivos CSV simple

Como ejemplo sencillo, considere un proveedor de tipos para acceder a datos científicos en formato Valor separado por comas (CSV). En esta sección se supone que los archivos CSV contienen una fila de encabezado seguida de datos de punto flotante, como se muestra en la tabla siguiente:

Distancia (metro) Tiempo (segundos)
50,0 3.7
100.0 5.2
150.0 6.4

En esta sección se muestra cómo proporcionar un tipo que puede usar para obtener filas con una Distance propiedad de tipo float<meter> y una Time propiedad de tipo float<second>. Por motivos de simplicidad, se realizan las siguientes suposiciones:

  • Los nombres de encabezado son sin unidad o tienen el formato "Nombre (unidad)" y no contienen comas.

  • Todas las unidades son del Sistema Internacional (SI) como define el módulo FSharp.Data.UnitSystems.SI.UnitNames Module (F#).

  • Todas las unidades son simples (por ejemplo, medidor) en lugar de compuestas (por ejemplo, medidor/segundo).

  • Todas las columnas contienen datos de punto flotante.

Un proveedor más completo aflojaría estas restricciones.

De nuevo, el primer paso es considerar cómo debería verse la API. Dado un info.csv archivo con el contenido de la tabla anterior (en formato separado por comas), los usuarios del proveedor deben poder escribir código similar al ejemplo siguiente:

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

En este caso, el compilador debe convertir estas llamadas en algo parecido al ejemplo siguiente:

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

La traducción óptima requerirá que el proveedor de tipos defina un tipo real CsvFile en el ensamblado del proveedor de tipos. Los proveedores de tipos suelen confiar en algunos tipos y métodos auxiliares para envolver la lógica clave. Dado que las medidas se borran en tiempo de ejecución, se puede utilizar float[] como el tipo de borrado para una fila. El compilador tratará diferentes columnas como tener tipos de medida diferentes. Por ejemplo, la primera columna de nuestro ejemplo tiene el tipo float<meter>y la segunda tiene float<second>. Sin embargo, la representación borrada puede seguir siendo bastante simple.

El código siguiente muestra el núcleo de la implementación.

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

Tenga en cuenta los siguientes puntos sobre la implementación:

  • Los constructores sobrecargados permiten leer el archivo original o uno que tenga un esquema idéntico. Este patrón es común al escribir un proveedor de tipos para orígenes de datos locales o remotos, y este patrón permite usar un archivo local como plantilla para datos remotos.

  • Puede usar el valor TypeProviderConfig que se pasa al constructor del proveedor de tipos para resolver nombres de archivo relativos.

  • Puede usar el AddDefinitionLocation método para definir la ubicación de las propiedades proporcionadas. Por lo tanto, si usa Go To Definition en una propiedad proporcionada, el archivo CSV se abrirá en Visual Studio.

  • Puede usar el ProvidedMeasureBuilder tipo para buscar las unidades de SI y para generar los tipos pertinentes float<_> .

Lecciones clave

En esta sección se explica cómo crear un proveedor de tipos para un origen de datos local con un esquema simple contenido en el propio origen de datos.

Ir más lejos

En las secciones siguientes se incluyen sugerencias para un estudio adicional.

Un vistazo al código compilado para tipos borrados

Para darte una idea de cómo corresponde el uso del proveedor de tipos al código emitido, examine la siguiente función mediante el HelloWorldTypeProvider que se usa anteriormente en este tema.

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

Esta es una imagen del código resultante descompilado mediante 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

Como se muestra en el ejemplo, todas las menciones del tipo Type1 y la propiedad InstanceProperty se han borrado, dejando solo las operaciones de los tipos en tiempo de ejecución involucrados.

Convenciones de diseño y nomenclatura para proveedores de tipos

Observe las siguientes convenciones al crear proveedores de tipos.

Proveedores para protocolos de conectividad En general, los nombres de la mayoría de los archivos DLL de proveedor para los protocolos de conectividad de datos y servicios, como las conexiones OData o SQL, deben finalizar en TypeProvider o TypeProviders. Por ejemplo, use un nombre DLL similar a la cadena siguiente:

Fabrikam.Management.BasicTypeProviders.dll

Asegúrese de que los tipos proporcionados son miembros del espacio de nombres correspondiente e indique el protocolo de conectividad que implementó:

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

Proveedores de utilidades para codificación general. En el caso de un proveedor de tipos de utilidad como el de expresiones regulares, el proveedor de tipos puede formar parte de una biblioteca base, como se muestra en el ejemplo siguiente:

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

En este caso, el tipo proporcionado aparecería en un punto adecuado según las convenciones de diseño normales de .NET:

  open Fabrikam.Core.Text.RegexTyped

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

Orígenes de datos Singleton. Algunos proveedores de tipos se conectan a un único origen de datos dedicado y solo proporcionan datos. En este caso, debe quitar el TypeProvider sufijo y usar convenciones normales para la nomenclatura de .NET:

#r "Fabrikam.Data.Freebase.dll"

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

Para obtener más información, vea la GetConnection convención de diseño que se describe más adelante en este tema.

Patrones de diseño para proveedores de tipos

En las secciones siguientes se describen los patrones de diseño que puede usar al crear proveedores de tipos.

El patrón de diseño GetConnection

La mayoría de los proveedores de tipos deben escribirse para usar el GetConnection patrón que usan los proveedores de tipos en FSharp.Data.TypeProviders.dll, como se muestra en el ejemplo siguiente:

#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

Proveedores de tipos respaldados por datos y servicios remotos

Antes de crear un proveedor de tipos respaldado por datos y servicios remotos, se deben tener en cuenta varios problemas que son inherentes a la programación conectada. Estos problemas incluyen las siguientes consideraciones:

  • mapeo de esquemas

  • Vida e invalidación en presencia de un cambio de esquema

  • Almacenamiento en caché de esquemas

  • implementaciones asincrónicas de operaciones de acceso a datos

  • consultas auxiliares, incluidas las consultas LINQ

  • credenciales y autenticación

En este tema no se exploran aún más estos problemas.

Técnicas de redacción adicionales

Al escribir tus propios proveedores de tipos, es posible que desees utilizar las siguientes técnicas adicionales.

Creación de tipos y miembros a petición

La API ProvidedType ha retrasado las versiones de AddMember.

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

Estas versiones se utilizan para crear espacios de tipos a petición.

Proporcionar tipos de matriz y instancias de tipos genéricos

Los miembros proporcionados (cuyas signaturas incluyen tipos de matriz, tipos byref y creaciones de instancias de tipos genéricos) se crean mediante el uso de los métodos MakeArrayType, MakePointerType y MakeGenericType normales en cualquier instancia de Type, incluyendo ProvidedTypeDefinitions.

Nota:

En algunas ocasiones, puede que tenga que usar el asistente de ProvidedTypeBuilder.MakeGenericType. Consulte la documentación del SDK del proveedor de tipos para obtener más información.

Proveer anotaciones para unidades de medida

La API ProvidedTypes proporciona asistentes para proporcionar anotaciones de medida. Por ejemplo, para proporcionar el tipo float<kg>, use el código siguiente:

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

Para proporcionar el tipo Nullable<decimal<kg/m^2>>, use el código siguiente:

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

Acceso a recursos de Project-Local o Script-Local

Durante la construcción, cada instancia de un proveedor de tipos puede recibir un valor TypeProviderConfig. Este valor contiene la "carpeta de resolución" para el proveedor (es decir, la carpeta del proyecto para la compilación o el directorio que contiene un script), la lista de ensamblados a los que se hace referencia y otra información.

Invalidación

Los proveedores pueden generar señales de invalidación para notificar al servicio de lenguaje F# que pueden haber cambiado las suposiciones de esquema. Cuando se produce una invalidación, se realiza nuevamente una comprobación de tipo si el proveedor se aloja en Visual Studio. Esta señal se omitirá cuando el proveedor esté hospedado en F# Interactive o por el compilador de F# (fsc.exe).

Almacenar en caché la información del esquema

A menudo, los proveedores deben almacenar en caché el acceso a la información de esquema. Los datos almacenados en caché deben almacenarse mediante un nombre de archivo que se da como parámetro estático o como datos de usuario. Un ejemplo de almacenamiento en caché de esquemas es el parámetro LocalSchemaFile en los proveedores de tipos del ensamblado FSharp.Data.TypeProviders. En la implementación de estos proveedores, este parámetro estático dirige al proveedor de tipos para usar la información de esquema en el archivo local especificado en lugar de acceder al origen de datos a través de la red. Para usar la información de esquema almacenada en caché, también debe establecer el parámetro estático ForceUpdate en false. Puede usar una técnica similar para habilitar el acceso a datos en línea y sin conexión.

Ensamblado de respaldo

Al compilar un archivo .dll o .exe, el archivo auxiliar .dll para los tipos generados se vincula estáticamente al ensamblado resultante. Este enlace se crea copiando las definiciones de tipo de Lenguaje Intermedio (IL) y cualquier recurso gestionado del ensamblado de respaldo al ensamblado final. Cuando se usa F# Interactive, el archivo de copia de seguridad .dll no se copia y, en su lugar, se carga directamente en el proceso interactivo de F#.

Excepciones y diagnósticos de proveedores de tipo

Todos los usos de todos los miembros de los tipos proporcionados pueden producir excepciones. En todos los casos, si un proveedor de tipos produce una excepción, el compilador de host atribuye el error a un proveedor de tipos específico.

  • Las excepciones de proveedor de tipos nunca deben dar lugar a errores internos del compilador.

  • Los proveedores de tipo no pueden notificar advertencias.

  • Cuando se hospeda un proveedor de tipos en el compilador de F#, un entorno de desarrollo de F# o F# Interactive, se detectan todas las excepciones de ese proveedor. La propiedad Message es siempre el texto del error y no aparece ningún seguimiento de pila. Si va a lanzar una excepción, puede lanzar los siguientes ejemplos: System.NotSupportedException, System.IO.IOException, System.Exception.

Proporcionar tipos generados

Hasta ahora, en este documento se ha explicado cómo proporcionar tipos borrados. También puede usar el mecanismo de proveedor de tipos en F# para proporcionar tipos generados, que se agregan como definiciones de tipos de .NET reales en el programa de los usuarios. Debe hacer referencia a los tipos generados proporcionados utilizando una definición de tipo.

open Microsoft.FSharp.TypeProviders

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

El código auxiliar ProvidedTypes-0.2 que forma parte de la versión F# 3.0 solo tiene compatibilidad limitada para proporcionar tipos generados. Las declaraciones siguientes deben ser verdaderas para una definición de tipo generada:

  • isErased se debe establecer en false.

  • El tipo generado debe agregarse a un objeto recién construido ProvidedAssembly(), que representa un contenedor para los fragmentos de código generados.

  • El proveedor debe tener un ensamblado que tenga un archivo .dll de respaldo real de .NET con un archivo .dll coincidente en el disco.

Reglas y limitaciones

Al escribir proveedores de tipos, tenga en cuenta las siguientes reglas y limitaciones.

Los tipos proporcionados deben ser accesibles

Debe tenerse acceso a todos los tipos proporcionados desde los tipos no anidados. Los tipos no anidados se proporcionan en la llamada al constructor TypeProviderForNamespaces o en una llamada a AddNamespace. Por ejemplo, si el proveedor proporciona un tipo StaticClass.P : T, debe asegurarse de que T es un tipo no anidado o está anidado debajo de uno.

Por ejemplo, algunos proveedores tienen una clase estática como DataTypes que contienen estos T1, T2, T3, ... tipos. De lo contrario, el error indica que se encontró una referencia al tipo T en el ensamblado A, pero no se encontró el tipo en ese ensamblado. Si aparece este error, compruebe que se puede acceder a todos los subtipos desde los tipos de proveedor. Nota: a estos tipos T1, T2, T3... se les denomina tipos sobre la marcha. Recuerde colocarlos en un espacio de nombres accesible o en un tipo primario.

Limitaciones del mecanismo de proveedores de tipos

El mecanismo de proveedores de tipos de F# tiene las siguientes limitaciones:

  • La infraestructura subyacente para los proveedores de tipos en F# no admite tipos genéricos proporcionados ni métodos genéricos proporcionados.

  • El mecanismo no admite tipos anidados con parámetros estáticos.

Sugerencias de desarrollo

Es posible que encuentre las siguientes sugerencias útiles durante el proceso de desarrollo:

Ejecución de dos instancias de Visual Studio

Puede desarrollar el proveedor de tipos en una instancia y probar el proveedor en el otro porque el IDE de prueba tomará un bloqueo en el archivo .dll que impide que se vuelva a compilar el proveedor de tipos. Por lo tanto, debe cerrar la segunda instancia de Visual Studio mientras el proveedor está integrado en la primera instancia y, a continuación, debe volver a abrir la segunda instancia una vez compilado el proveedor.

Depure proveedores de tipo mediante llamadas a fsc.exe.

Puede invocar proveedores de tipos mediante las siguientes herramientas:

  • fsc.exe (compilador de línea de comandos de F#)

  • fsi.exe (compilador interactivo de F#)

  • devenv.exe (Visual Studio)

A menudo, lo más fácil es depurar los proveedores de tipo mediante fsc.exe en un archivo de script de prueba (por ejemplo, script.fsx). Puede iniciar un depurador desde la línea de comandos.

devenv /debugexe fsc.exe script.fsx

Puede usar print-to-stdout como registro.

Consulte también