Tutorial: criar um provedor de tipos

O mecanismo de provedor de tipos em F# é uma parte significativa do suporte dessa plataforma para programação avançada em informações. Este tutorial explica como criar provedores de tipos próprios orientando você no desenvolvimento de vários provedores de tipos simples a fim de ilustrar os conceitos básicos. Para obter mais informações sobre o mecanismo de provedor de tipos em F#, confira Provedores de Tipos.

O ecossistema F# contém uma série de provedores de tipos para serviços de dados corporativos e de Internet usados com frequência. Por exemplo:

  • O FSharp.Data inclui provedores de tipos para formatos de documento JSON, XML, CSV e HTML.

  • O SwaggerProvider inclui dois provedores de tipos generativos que geram o modelo de objeto e os clientes HTTP para APIs descritos pelos esquemas OpenApi 3.0 e Swagger 2.0.

  • O FSharp.Data.SqlClient tem um conjunto de provedores de tipos para a inserção marcada em tempo de compilação do T-SQL em F#.

Você pode criar provedores de tipo personalizados ou pode fazer referência a provedores de tipo criados por outros. Por exemplo, suponha que sua organização tenha um serviço de dados que forneça um crescente número de conjuntos de dados nomeados, cada um com um esquema próprio de dados estáveis. Você pode criar um provedor de tipos que leia os esquemas e apresente os conjuntos de dados atuais para o programador de uma forma fortemente tipada.

Antes de iniciar

O mecanismo de provedor de tipos foi projetado principalmente para injetar espaços de informações de serviço e dados estáveis na experiência de programação do F#.

Esse mecanismo não foi projetado para injetar espaços de informações cujo esquema é alterado durante a execução do programa de maneiras relevantes para a lógica do programa. Além disso, o mecanismo não foi projetado para meta-programação dentro da linguagem, embora esse domínio contenha alguns usos válidos. Você deve usar esse mecanismo somente quando necessário e onde o desenvolvimento de um provedor de tipos gera um valor muito alto.

Você deve evitar escrever um provedor de tipos em que um esquema não esteja disponível. Da mesma forma, você deve evitar escrever um provedor de tipos em que uma biblioteca .NET comum (ou até mesmo existente) seria suficiente.

Antes de começar, você pode fazer as seguintes perguntas:

  • Você tem um esquema para sua fonte de informações? Nesse caso, qual é o mapeamento para o sistema de tipos F# e .NET?

  • Você pode usar uma API existente (tipada dinamicamente) como ponto de partida para sua implementação?

  • Você e sua organização terão usos suficientes do provedor de tipos para fazer a gravação valer a pena? Uma biblioteca .NET normal atenderia às suas necessidades?

  • Quanto seu esquema será alterado?

  • Ele será alterado durante a codificação?

  • Ele será alterado entre as sessões de codificação?

  • Ele será alterado durante a execução do programa?

Os provedores de tipos são mais adequados para situações em que o esquema é estável em tempo de execução e durante o tempo de vida do código compilado.

Um provedor de tipos simples

Este exemplo do Samples.HelloWorldTypeProvider, é semelhante aos exemplos no diretório examples do SDK do Provedor de Tipos F#. O provedor disponibiliza um "espaço de tipos" que contém 100 tipos apagados, como mostra o código a seguir usando a sintaxe de assinatura F# e omitindo os detalhes de todos, exceto Type1. Para obter mais informações sobre tipos apagados, confira Detalhes sobre tipos fornecidos apagados mais adiante neste tópico.

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

Observe que o conjunto de tipos e membros fornecidos é estaticamente conhecido. Este exemplo não aproveita a capacidade dos provedores de fornecer tipos que dependem de um esquema. A implementação do provedor de tipos é descrita no código a seguir e os detalhes são abordados em seções posteriores deste tópico.

Aviso

Pode haver diferenças entre esse código e os exemplos 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()

Para usar esse provedor, abra uma instância separada do Visual Studio, crie um script F# e adicione uma referência ao provedor do script usando #r, como mostra o seguinte código:

#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

Em seguida, procure os tipos no namespace Samples.HelloWorldTypeProvider que o provedor de tipos gerou.

Antes de recompilar o provedor, verifique se você fechou todas as instâncias do Visual Studio e do F# Interativo que estão usando a DLL do provedor. Caso contrário, ocorrerá um erro de compilação porque a DLL de saída será bloqueada.

Para depurar esse provedor usando instruções de impressão, faça um script que exponha um problema com o provedor e use o seguinte código:

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

Para depurar esse provedor usando o Visual Studio, abra o Prompt de Comando do Desenvolvedor para Visual Studio com credenciais administrativas e execute o seguinte comando:

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

Como alternativa, abra o Visual Studio, acesse o menu Depurar, escolha Debug/Attach to process… e anexe a outro processo devenv em que você está editando seu script. Usando esse método, você pode direcionar mais facilmente uma lógica específica no provedor de tipos digitando interativamente expressões na segunda instância (com IntelliSense completo e outros recursos).

Você pode desabilitar a depuração Apenas Meu Código para identificar melhor os erros no código gerado. Para obter informações sobre como habilitar ou desabilitar esse recurso, confira Navegar pelo Código com o Depurador. Além disso, você também pode definir a captura de exceção de primeira chance abrindo o menu Debug e escolhendo Exceptions ou selecionando as teclas Ctrl+Alt+E para abrir a caixa de diálogo Exceptions. Nessa caixa de diálogo, em Common Language Runtime Exceptions, marque a caixa de seleção Thrown.

Implementação do Provedor de Tipos

Esta seção orienta você nas seções principais da implementação do provedor de tipo. Primeiro, você define o tipo do próprio provedor de tipos personalizado:

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

Esse tipo deve ser público e você deve marcá-lo com o atributo TypeProvider para que o compilador reconheça o provedor de tipos quando outro projeto F# tentar referenciar o assembly que contém o tipo. O parâmetro config é opcional e, se presente, contém informações de configuração contextuais para a instância do provedor de tipos que o compilador F# cria.

Em seguida, você implementa a interface ITypeProvider. Nesse caso, você usa o tipo TypeProviderForNamespaces da API ProvidedTypes como um tipo base. Esse tipo auxiliar pode fornecer uma coleção finita de namespaces fornecidos antecipadamente, cada um dos quais contém diretamente um número finito de tipos fixos fornecidos antecipadamente. Nesse contexto, o provedor gera tipos antecipadamente, mesmo que não sejam necessários ou usados.

inherit TypeProviderForNamespaces(config)

Em seguida, defina valores privados locais que especificam o namespace para os tipos fornecidos e localize o próprio assembly do provedor de tipos. Esse assembly é usado posteriormente como o tipo pai lógico dos tipos fornecidos apagados.

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

Em seguida, crie uma função para fornecer cada um dos tipos Type1... Tipo100. Essa função é explicada mais detalhadamente mais adiante neste tópico.

let makeOneProvidedType (n:int) = …

Em seguida, gere os 100 tipos fornecidos:

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

Então, adicione os tipos como um namespace fornecido:

do this.AddNamespace(namespaceName, types)

Por fim, adicione um atributo de assembly que indica que você está criando uma DLL do provedor de tipos:

[<assembly:TypeProviderAssembly>]
do()

Fornecendo um tipo e seus membros

A função makeOneProvidedType faz o trabalho real de fornecer um dos tipos.

let makeOneProvidedType (n:int) =
…

Esta etapa explica a implementação dessa função. Primeiro, crie o tipo fornecido (por exemplo, Type1, quando n = 1 ou 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>)

Você deve observar os seguintes pontos:

  • Esse tipo fornecido é apagado. Como você indica que o tipo base é obj, as instâncias aparecerão como valores do tipo obj no código compilado.

  • Ao especificar um tipo não aninhado, você deve especificar o assembly e o namespace. Para tipos apagados, o assembly deve ser o próprio assembly do provedor de tipos.

Em seguida, adicione a documentação XML ao tipo. Essa documentação é atrasada, ou seja, calculada sob demanda se o compilador de host precisar dela.

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

Em seguida, adicione uma propriedade estática fornecida ao tipo:

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

Obter essa propriedade sempre resultará na cadeia de caracteres "Olá!". A GetterCode da propriedade usa uma citação da F#, que representa o código que o compilador de host gera para obter a propriedade. Para obter mais informações sobre citações, confira Citações de Código (F#).

Adicione a documentação XML à propriedade.

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

Agora, anexe a propriedade fornecida ao tipo fornecido. Você deve anexar um membro fornecido a apenas um tipo. Caso contrário, o membro nunca estará acessível.

t.AddMember staticProp

Agora, crie um construtor fornecido que não usa parâmetros.

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

O InvokeCode do construtor retorna uma citação do F#, que representa o código que o compilador de host gera quando o construtor é chamado. Por exemplo, você pode usar o seguinte construtor:

new Type10()

Uma instância do tipo fornecido será criada com os dados subjacentes "Os dados do objeto". O código entre aspas inclui uma conversão para obj porque esse tipo é a eliminação desse tipo fornecido (como você especificou quando declarou o tipo fornecido).

Adicione a documentação XML ao construtor e adicione o construtor fornecido ao tipo fornecido:

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

t.AddMember ctor

Crie um segundo construtor fornecido que usa um parâmetro:

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

O construtor InvokeCode retorna novamente uma citação do F#, que representa o código que o compilador de host gerou para uma chamada ao método. Por exemplo, você pode usar o seguinte construtor:

new Type10("ten")

Uma instância do tipo fornecido é criada com os dados subjacentes "dez". Talvez você já tenha notado que a função InvokeCode retorna uma citação. A entrada para essa função é uma lista de expressões, uma por parâmetro do construtor. Nesse caso, uma expressão que representa o valor de parâmetro único está disponível em args[0]. O código de uma chamada ao construtor força o valor de retorno para o tipo objapagado. Depois de adicionar o segundo construtor fornecido ao tipo, você cria uma propriedade de instância fornecida:

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

Obter essa propriedade retornará o comprimento da cadeia de caracteres, que é o objeto de representação. A propriedade GetterCode retorna uma citação do F# que especifica o código que o compilador de host gera para obter a propriedade. Assim como InvokeCode, a função GetterCode retorna uma citação. O compilador de host chama essa função com uma lista de argumentos. Nesse caso, os argumentos incluem apenas a única expressão que representa a instância na qual o getter está sendo chamado, que você pode acessar usando args[0]. A implementação de GetterCode então se junta à citação resultante no tipo apagado obj e uma conversão é usada para atender ao mecanismo do compilador para verificar os tipos de que o objeto é uma cadeia de caracteres. A próxima parte de makeOneProvidedType fornece um método de instância com um 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 fim, crie um tipo aninhado que contenha 100 propriedades aninhadas. A criação desse tipo aninhado e das respectivas propriedades dele é atrasada, ou seja, computada sob demanda.

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

Detalhes sobre tipos fornecidos apagados

O exemplo nesta seção fornece apenas tipos fornecidos apagados, que são particularmente úteis nas seguintes situações:

  • Quando você está escrevendo um provedor para um espaço de informações que contém apenas dados e métodos.

  • Quando você está escrevendo um provedor em que a semântica precisa do tipo runtime não é essencial para o uso prático do espaço de informações.

  • Quando você está escrevendo um provedor para um espaço de informações tão grande e interconectado que tecnicamente não é viável gerar tipos reais do .NET para o espaço de informações.

Neste exemplo, cada tipo fornecido é apagado para o tipo obj e todos os usos do tipo aparecerão como tipo obj no código compilado. Na verdade, os objetos subjacentes nesses exemplos são cadeias de caracteres, mas o tipo aparecerá como System.Object no código compilado do .NET. Assim como acontece com todos os usos de apagamento de tipo, você pode usar a conversão boxing, conversão unboxing e conversão regular explícitas para subverter os tipos apagados. Nesse caso, pode ocorrer uma exceção de conversão inválida quando o objeto é usado. Um runtime de provedor pode definir um tipo próprio de representação privada para ajudar na proteção contra falsas representações. Você não pode definir tipos apagados no próprio F#. Somente tipos fornecidos podem ser apagados. Você deve entender as ramificações, práticas e semânticas, de usar tipos apagados para seu provedor de tipos ou um provedor que fornece tipos apagados. Um tipo apagado não tem um tipo .NET real. Portanto, você não pode fazer uma reflexão precisa sobre o tipo e pode subverter tipos apagados se usar conversões de runtime e outras técnicas que dependem da semântica exata do tipo de runtime. A subversão de tipos apagados frequentemente resulta em exceções de conversão de tipo em tempo de execução.

Como escolher representações para tipos fornecidos apagados

Para alguns usos de tipos fornecidos apagados, nenhuma representação é necessária. Por exemplo, o tipo fornecido apagado pode conter apenas propriedades estáticas e membros e nenhum construtor, e nenhum método ou propriedades retornaria uma instância do tipo. Se você puder acessar instâncias de um tipo fornecido apagado, considere as seguintes perguntas:

O que é o apagamento de um tipo fornecido?

  • O apagamento de um tipo fornecido é como o tipo aparece no código .NET compilado.

  • O apagamento de um tipo de classe apagado fornecido é sempre o primeiro tipo base não apagado na cadeia de herança do tipo.

  • O apagamento de um tipo de interface apagado fornecido é sempre System.Object.

Quais são as representações de um tipo fornecido?

  • O conjunto de objetos possíveis para um tipo fornecido apagado é chamado de representações do tipo. No exemplo neste documento, as representações de todos os tipos fornecidos apagados Type1..Type100 são sempre objetos de cadeia de caracteres.

Todas as representações de um tipo fornecido devem ser compatíveis com o apagamento do tipo fornecido. (Caso contrário, o compilador F# vai gerar um erro de uso do provedor de tipos ou será gerado um código .NET não verificável inválido. Um provedor de tipo não será válido se retornar o código que fornece uma representação que não seja válida.)

Você pode escolher uma representação para objetos fornecidos usando qualquer uma das seguintes abordagens, ambas muito comuns:

  • Se você estiver simplesmente fornecendo um wrapper fortemente tipado sobre um tipo .NET existente, geralmente faz sentido que seu tipo se apague para esse tipo, use instâncias desse tipo como representações ou ambos. Essa abordagem é apropriada quando a maioria dos métodos existentes nesse tipo ainda faz sentido ao usar a versão fortemente tipada.

  • Se você quiser criar uma API que difere significativamente de qualquer API .NET existente, faz sentido criar tipos de runtime que serão a eliminação de tipo e representações para os tipos fornecidos.

O exemplo neste documento usa cadeias de caracteres como representações de objetos fornecidos. Com frequência, pode ser apropriado usar outros objetos para representações. Por exemplo, você pode usar um dicionário como um recipiente de propriedades:

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

Como alternativa, você pode definir um tipo no provedor de tipos que será usado em tempo de execução para formar a representação, juntamente com uma ou mais operações de runtime:

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

Os membros fornecidos podem, então, construir instâncias desse tipo de objeto:

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

Nesse caso, você pode (opcionalmente) usar esse tipo como o apagamento de tipo especificando esse tipo como o baseType ao construir ProvidedTypeDefinition:

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

Principais lições

A seção anterior explicava como criar um provedor de tipo de apagamento simples que fornece um intervalo de tipos, propriedades e métodos. Esta seção também explicou o conceito de apagamento de tipo, incluindo algumas das vantagens e desvantagens de fornecer tipos apagados de um provedor de tipos e discutiu representações de tipos apagados.

Um provedor de tipo que usa parâmetros estáticos

A capacidade de parametrizar provedores de tipo por dados estáticos permite muitos cenários interessantes, mesmo nos casos em que o provedor não precisa acessar dados locais ou remotos. Nesta seção, você aprenderá algumas técnicas básicas para montar esse provedor.

Tipo de provedor Regex verificado

Imagine que você deseja implementar um provedor de tipos para expressões regulares que encapsula as bibliotecas .NET Regex em uma interface que fornece as seguintes garantias de tempo de compilação:

  • Como verificar se uma expressão regular é válida.

  • Fornecendo propriedades nomeadas em correspondências baseadas em nomes de grupo na expressão regular.

Esta seção mostra como usar provedores de tipos para criar um tipo RegexTyped que o padrão de expressão regular parametriza a fim de fornecer esses benefícios. O compilador relatará um erro se o padrão fornecido não for válido e o provedor de tipos poderá extrair os grupos do padrão para que você possa acessá-los usando propriedades nomeadas em correspondências. Ao criar um provedor de tipo, você deve considerar como a API exposta deve ser para os usuários finais e como esse design será traduzido para o código .NET. O seguinte exemplo mostra como usar essa API para obter os componentes do 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"

O seguinte exemplo mostra como o provedor de tipos converte essas chamadas:

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"

Observe os seguintes pontos:

  • O tipo Regex padrão representa o tipo parametrizado RegexTyped.

  • O construtor RegexTyped resulta em uma chamada para o construtor Regex, passando o argumento de tipo estático para o padrão.

  • Os resultados do método Match são representados pelo tipo padrão Match.

  • Cada grupo nomeado resulta em uma propriedade fornecida e acessar a propriedade resulta em um uso de um indexador na coleção de uma correspondência Groups.

O código a seguir é o núcleo da lógica para implementar esse provedor, e este exemplo omite a adição de todos os membros ao tipo fornecido. Para obter informações sobre cada membro adicionado, confira a seção apropriada mais adiante neste tópico.

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

Observe os seguintes pontos:

  • O provedor de tipo usa dois parâmetros estáticos: pattern, que é obrigatório, e options, que são opcionais (porque um valor padrão é fornecido).

  • Depois que os argumentos estáticos forem fornecidos, você criará uma instância da expressão regular. Essa instância vai gerar uma exceção se o Regex estiver malformado e esse erro for relatado aos usuários.

  • No retorno de chamada DefineStaticParameters, você define o tipo que será retornado depois que os argumentos forem fornecidos.

  • Esse código se define HideObjectMethods como true para que a experiência do IntelliSense permaneça simplificada. Esse atributo faz com que os membros Equals, GetHashCode, Finalize e GetType sejam suprimidos das listas do IntelliSense de um objeto fornecido.

  • Você usa obj como o tipo base do método, mas usará um objeto Regex como a representação de runtime desse tipo, como mostra o próximo exemplo.

  • A chamada para o construtor Regex gera um ArgumentException quando a expressão regular não é válida. O compilador captura essa exceção e relata uma mensagem de erro para o usuário no momento da compilação ou no editor do Visual Studio. Essa exceção permite que expressões regulares sejam validadas sem executar um aplicativo.

O tipo definido acima ainda não é útil porque não contém métodos ou propriedades significativas. Primeiro, adicione um 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

O código anterior define um método IsMatch, que usa uma cadeia de caracteres como entrada e retorna uma bool. A única parte complicada é o uso do argumento args dentro da definição InvokeCode. Neste exemplo, args é uma lista de citações que representa os argumentos para esse método. Se o método for um método de instância, o primeiro argumento representará o argumento this. No entanto, para um método estático, os argumentos são apenas os argumentos explícitos do método. Observe que o tipo do valor citado deve corresponder ao tipo de retorno especificado (nesse caso, bool). Observe também que esse código usa o método AddXmlDoc para garantir que o método fornecido também tenha uma documentação útil, que você pode fornecer por meio do IntelliSense.

Em seguida, adicione um método de correspondência de instância. No entanto, esse método deve retornar um valor de um tipo fornecido Match para que os grupos possam ser acessados de maneira fortemente tipada. Assim, primeiro você declara o tipo Match. Como esse tipo depende do padrão fornecido como um argumento estático, ele deve ser aninhado dentro da definição de tipo parametrizado:

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

ty.AddMember matchTy

Em seguida, você adiciona uma propriedade ao tipo de correspondência de cada grupo. Em tempo de execução, uma correspondência é representada como um valor Match, portanto, a citação que define a propriedade deve usar a propriedade indexada Groups para obter o grupo relevante.

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

Novamente, observe que você está adicionando a documentação XML à propriedade fornecida. Observe também que uma propriedade pode ser lida se uma função GetterCode for fornecida e a propriedade poderá ser gravada se uma função SetterCode for fornecida, portanto, a propriedade resultante será somente leitura.

Agora você pode criar um método de instância que retorna um valor desse tipo Match:

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

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

ty.AddMember matchMeth

Como você está criando um método de instância, args[0] representa a instância RegexTyped na qual o método está sendo chamado e args[1] é o argumento de entrada.

Por fim, forneça um construtor para que as instâncias do tipo fornecido possam ser criadas.

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

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

ty.AddMember ctor

O construtor simplesmente apaga a criação de uma instância padrão do Regex do .NET, que é novamente demarcada em um objeto porque obj é o apagamento do tipo fornecido. Com essa alteração, o uso de API de exemplo especificado anteriormente no tópico funciona conforme o esperado. O seguinte código é completo e final:

namespace Samples.FSharp.RegexTypeProvider

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

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

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

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

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

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

                // Create an instance of the regular expression.

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

                // Declare the typed regex provided type.

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

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

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

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

                ty.AddMember isMatch

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

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

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

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

                ty.AddMember matchMeth

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

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

                ty.AddMember ctor

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

    do this.AddNamespace(rootNamespace, [regexTy])

[<TypeProviderAssembly>]
do ()

Principais lições

Esta seção explicou como criar um provedor de tipos que opera sobre os parâmetros estáticos dele. O provedor verifica o parâmetro estático e fornece operações com base no valor dele.

Um provedor de tipos que é apoiado por dados locais

Frequentemente, você pode querer que os provedores de tipos apresentem APIs com base não apenas em parâmetros estáticos, mas também em informações de sistemas locais ou remotos. Esta seção discute provedores de tipos baseados em dados locais, como arquivos de dados locais.

Provedor de arquivos CSV simples

Como um exemplo simples, considere um provedor de tipos para acessar dados científicos no formato CSV (Valor Separado por Vírgulas). Esta seção pressupõe que os arquivos CSV contenham uma linha de cabeçalho seguida de dados de ponto flutuante, como ilustra a seguinte tabela:

Distância (metro) Hora (segundo)
50,0 3.7
100.0 5.2
150.0 6.4

Esta seção mostra como fornecer um tipo que você pode usar para obter linhas com uma propriedade Distance do tipo float<meter> e uma propriedade Time do tipo float<second>. Para simplificar, as seguintes suposições são feitas:

  • Os nomes de cabeçalho são sem unidade ou têm o formulário "Nome (unidade)" e não contêm vírgulas.

  • As unidades são todas unidades do System International (SI), como define o módulo Data.UnitSystems.SI.UnitNames (F#).

  • As unidades são todas simples (por exemplo, metro) em vez de compostas (por exemplo, metro/segundo).

  • Todas as colunas contêm dados de ponto flutuante.

Um provedor mais completo atenuaria essas restrições.

Novamente, a primeira etapa é considerar a aparência da API. Dado um arquivo info.csv com o conteúdo da tabela anterior (em formato separado por vírgulas), os usuários do provedor devem ser capazes de escrever código semelhante ao seguinte exemplo:

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

Nesse caso, o compilador deve converter essas chamadas em algo semelhante ao seguinte exemplo:

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

A tradução ideal exigirá que o provedor de tipos defina um tipo real CsvFile no assembly do provedor de tipos. Os provedores de tipos geralmente dependem de alguns tipos auxiliares e métodos para encapsular uma lógica importante. Como as medidas são apagadas em tempo de execução, você pode usar um float[] como o tipo apagado de uma linha. O compilador tratará diferentes colunas como tendo tipos de medida diferentes. Por exemplo, a primeira coluna em nosso exemplo tem tipo float<meter> e a segunda tem tipo float<second>. No entanto, a representação apagada pode permanecer bastante simples.

O seguinte código mostra a implementação dessa classe.

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

Observe os seguintes pontos sobre a implementação:

  • Construtores sobrecarregados permitem que o arquivo original ou aquele que tem um esquema idêntico seja lido. Esse padrão é comum quando você escreve um provedor de tipo para fontes de dados locais ou remotas, e esse padrão permite que um arquivo local seja usado como modelo para dados remotos.

  • Você pode usar o valor TypeProviderConfig que é passado para o construtor do provedor de tipo para resolver nomes de arquivo relativos.

  • Você pode usar o método AddDefinitionLocation para definir o local das propriedades fornecidas. Portanto, se você usar Go To Definition em uma propriedade fornecida, o arquivo CSV será aberto no Visual Studio.

  • Você pode usar o tipo ProvidedMeasureBuilder para pesquisar as unidades do SI e gerar os tipos relevantes float<_>.

Principais lições

Esta seção explicou como criar um provedor de tipos para uma fonte de dados local com um esquema simples contido na própria fonte de dados.

Aprofundamento

As seções a seguir incluem sugestões para mais estudos.

Uma olhada no código compilado para tipos apagados

Para dar uma ideia de como o uso do provedor de tipos corresponde ao código emitido, examine a função a seguir usando o HelloWorldTypeProvider empregado anteriormente neste tópico.

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

Aqui está uma imagem do código resultante descompilado 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

Como mostra o exemplo, todas as menções do tipo Type1 e da propriedade InstanceProperty foram apagadas, deixando apenas operações nos tipos de runtime envolvidos.

Convenções de design e nomenclatura para provedores de tipo

Observe as convenções a seguir ao criar provedores de tipo.

Provedores de Protocolos de Conectividade Em geral, os nomes da maioria das DLLs do provedor para protocolos de conectividade de dados e serviços, como conexões OData ou SQL, devem terminar em TypeProvider ou TypeProviders. Por exemplo, use um nome de DLL semelhante à seguinte cadeia de caracteres:

Fabrikam.Management.BasicTypeProviders.dll

Verifique se os tipos fornecidos são membros do namespace correspondente e indique o protocolo de conectividade implementado:

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

Provedores de utilitários para codificação geral. Para um provedor de tipos de utilitário como esse para expressões regulares, o provedor de tipos pode fazer parte de uma biblioteca base, como mostra o seguinte exemplo:

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

Nesse caso, o tipo fornecido aparecerá em um ponto apropriado de acordo com convenções normais de design do .NET:

  open Fabrikam.Core.Text.RegexTyped

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

Fontes de dados Singleton. Alguns provedores de tipos se conectam a apenas uma fonte de dados dedicada e fornecem apenas dados. Nesse caso, você deve descartar o sufixo TypeProvider e usar convenções normais para nomenclatura do .NET:

#r "Fabrikam.Data.Freebase.dll"

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

Para obter mais informações, confira a convenção de design GetConnection descrita posteriormente neste tópico.

Padrões de design para provedores de tipo

As seções a seguir descrevem padrões de design que você pode usar ao criar provedores de tipos.

O padrão de design GetConnection

A maioria dos provedores de tipos deve ser gravada para usar o padrão GetConnection, usado pelos provedores de tipos na FSharp.Data.TypeProviders.dll, como mostra o seguinte exemplo:

#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

Provedores de tipos apoiados por dados e serviços remotos

Antes de criar um provedor de tipos com suporte de dados e serviços remotos, você deve considerar uma série de problemas inerentes à programação conectada. Esses problemas incluem as seguintes considerações:

  • Mapeamento de esquema

  • Dinamismo e invalidação na presença de alterações no esquema

  • Armazenamento em cache do esquema

  • Implementações assíncronas de operações de acesso a dados

  • Consultas de apoio, incluindo consultas LINQ

  • Credenciais e autenticação

Este tópico não explora mais esses problemas.

Técnicas de criação adicionais

Ao escrever seus próprios provedores de tipos, convém usar as seguintes técnicas adicionais.

Criando tipos e membros sob demanda

A API ProvidedType atrasou as versões do AddMember.

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

Essas versões são usadas para criar espaços de tipos sob demanda.

Fornecendo tipos de matriz e instanciações de tipo genérico

Você faz membros fornecidos (cujas assinaturas incluem tipos de matriz, tipos byref e instanciações de tipos genéricos) usando o normal MakeArrayType, MakePointerType e MakeGenericType em qualquer instância de Type, incluindo ProvidedTypeDefinitions.

Observação

Em alguns casos, talvez seja necessário usar o auxiliar em ProvidedTypeBuilder.MakeGenericType. Confira a documentação do SDK do Provedor de Tipos para obter mais detalhes.

Fornecendo uma unidade de anotações de medida

A API ProvidedTypes fornece auxiliares para fornecer anotações de medida. Por exemplo, para fornecer o tipo float<kg>, use o seguinte código:

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

Para fornecer o tipo Nullable<decimal<kg/m^2>>, use o seguinte código:

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

Acessando recursos de Project-Local ou Script-Local

Cada instância de um provedor de tipo pode receber um valor TypeProviderConfig durante a construção. Esse valor contém a "pasta de resolução" do provedor (ou seja, a pasta do projeto para a compilação ou o diretório que contém um script), a lista de assemblies referenciados e outras informações.

Invalidação

Os provedores podem gerar sinais de invalidação para notificar o serviço da linguagem F# de que as suposições de esquema podem ter sido alteradas. Quando ocorre a invalidação, a verificação de tipo é refeita se o provedor está sendo hospedado no Visual Studio. Esse sinal será ignorado quando o provedor estiver hospedado no F# Interativo ou pelo Compilador F# (fsc.exe).

Informações do esquema de cache

Os provedores geralmente devem armazenar em cache o acesso às informações do esquema. Os dados armazenados em cache devem ser armazenados usando um nome de arquivo fornecido como um parâmetro estático ou como dados do usuário. Um exemplo de cache de esquema é o parâmetro LocalSchemaFile nos provedores de tipos do assembly FSharp.Data.TypeProviders. Na implementação desses provedores, esse parâmetro estático orienta o provedor de tipos a usar as informações de esquema no arquivo local especificado em vez de acessar a fonte de dados pela rede. Para usar informações de esquema em cache, você também deve definir o parâmetro estático ForceUpdate como false. Você pode usar uma técnica semelhante para habilitar o acesso a dados online e offline.

Assembly de suporte

Quando você compila um arquivo .dll ou .exe, o arquivo de .dll de suporte dos tipos gerados é vinculado estaticamente ao assembly resultante. Esse link é criado copiando as definições de tipo IL (Linguagem Intermediária) e todos os recursos gerenciados do assembly de suporte para o assembly final. Quando você usa o F# Interativo, o arquivo de .dll de suporte não é copiado se, em vez disso, é carregado diretamente no processo do F# Interativo.

Exceções e diagnósticos de provedores de tipos

Todos os usos de todos os membros de tipos fornecidos podem gerar exceções. Em todos os casos, se um provedor de tipos gerar uma exceção, o compilador de host atribui o erro a um provedor de tipos específico.

  • As exceções do provedor de tipos nunca devem resultar em erros internos do compilador.

  • Os provedores de tipos não podem relatar avisos.

  • Quando um provedor de tipo é hospedado no compilador F#, em um ambiente de desenvolvimento F# ou no F# Interativo, todas as exceções desse provedor são capturadas. A propriedade Message é sempre o texto de erro e nenhum rastreamento de pilha é exibido. Se você vai gerar uma exceção, é possível gerar os seguintes exemplos: System.NotSupportedException,System.IO.IOException e System.Exception.

Fornecendo tipos gerados

Até agora, este documento explicou como fornecer tipos apagados. Você também pode usar o mecanismo de provedor de tipos em F# para fornecer tipos gerados, que são adicionados como definições reais de tipo .NET no programa dos usuários. Você deve se referir aos tipos fornecidos gerados usando uma definição de tipo.

open Microsoft.FSharp.TypeProviders

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

O código auxiliar ProvidedTypes-0.2 que faz parte da versão F# 3.0 tem suporte limitado apenas para fornecer tipos gerados. As seguintes instruções devem ser verdadeiras para uma definição de tipo gerada:

  • isErased deve ser definido como false.

  • O tipo gerado deve ser adicionado a um ProvidedAssembly() recém-construído, que representa um contêiner para fragmentos de código gerados.

  • O provedor deve ter um assembly que tenha um arquivo .NET .dll de suporte real com um arquivo de .dll correspondente no disco.

Regras e limitações

Ao escrever provedores de tipos, tenha em mente as regras e limitações a seguir.

Os tipos fornecidos devem ser acessíveis

Todos os tipos fornecidos devem ser acessíveis por meio dos tipos não aninhados. Os tipos não aninhados são dados na chamada do construtor TypeProviderForNamespaces ou na chamada de AddNamespace. Por exemplo, se o provedor fornecer um tipo StaticClass.P : T, você deverá garantir que T seja um tipo não aninhado ou aninhado em apenas um.

Por exemplo, alguns provedores têm uma classe estática, como DataTypes, a que contém esses tipos T1, T2, T3, .... Caso contrário, o erro diz que uma referência ao tipo T no assembly A foi encontrada, mas o tipo não pôde ser encontrado nesse assembly. Se esse erro for exibido, verifique se todos os seus subtipos podem ser acessados por meio dos tipos do provedor. Observação: esses tipos T1, T2, T3... são chamados de tipos on-the-fly. Lembre-se de colocá-los em um namespace acessível ou em um tipo pai.

Limitações do mecanismo de provedor de tipos

O mecanismo de provedor de tipos em F# tem as seguintes limitações:

  • A infraestrutura subjacente para provedores de tipo no F# não dá suporte a tipos genéricos fornecidos ou métodos genéricos fornecidos.

  • O mecanismo não dá suporte a tipos aninhados com parâmetros estáticos.

Dicas de desenvolvimento

Você pode achar as seguintes dicas úteis durante o processo de desenvolvimento:

Executar duas instâncias do Visual Studio

Você pode desenvolver o provedor de tipos em uma instância e testar o provedor na outra porque o IDE de teste terá um bloqueio no arquivo .dll que impede que o provedor de tipos seja recriado. Portanto, você deve fechar a segunda instância do Visual Studio enquanto o provedor é criado na primeira instância e, em seguida, você deve reabrir a segunda instância após a criação do provedor.

Depurar provedores de tipos usando invocações de fsc.exe

Você pode invocar provedores de tipos usando as seguintes ferramentas:

  • fsc.exe (compilador de linha de comando do F#)

  • fsi.exe (compilador do F# Interativo)

  • devenv.exe (Visual Studio)

Geralmente, você pode depurar provedores de tipo mais facilmente usando fsc.exe em um arquivo de script de teste (por exemplo, script.fsx). Você pode iniciar um depurador por meio de um prompt de comando.

devenv /debugexe fsc.exe script.fsx

Você pode usar o registro em log de impressão para stdout.

Confira também