Compartilhar via


Tutorial: Criar um provedor de tipos

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

O ecossistema F# contém uma variedade 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.

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

  • FSharp.Data.SqlClient possui um conjunto de provedores de tipo para incorporação verificada em tempo de compilação do T-SQL em F#.

Você pode criar provedores de tipos personalizados ou referenciar provedores de tipo que outras pessoas criaram. Por exemplo, sua organização pode ter um serviço de dados que fornece um grande e crescente número de conjuntos de dados nomeados, cada um com seu próprio esquema de dados estável. 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 do provedor de tipos foi projetado principalmente para injetar dados estáveis e espaços de informações de serviço na experiência de programação F#.

Esse mecanismo não foi projetado para injetar espaços de informações cujo esquema muda 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 intra-idioma, mesmo que 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.

Evite 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 o seu esquema mudará?

  • Isso mudará durante a codificação?

  • Isso mudará entre as sessões de codificação?

  • Isso mudará 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 é Samples.HelloWorldTypeProvider, semelhante aos exemplos no examples diretório do SDK do Provedor de Tipo F#. O provedor disponibiliza um "espaço de tipo" que contém 100 tipos apagados, como mostra o código a seguir usando a sintaxe de assinatura F# e omitindo os detalhes para todos, exceto Type1. Para obter mais informações sobre tipos apagados, consulte Detalhes sobre tipos fornecidos apagados posteriormente 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 usando #r como mostra o código a seguir:

#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 Samples.HelloWorldTypeProvider namespace gerado pelo provedor de tipos.

Antes de recompilar o provedor, verifique se você fechou todas as instâncias do Visual Studio e do F# Interactive que estão usando a DLL do provedor. Caso contrário, ocorrerá um erro de build 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, abra o menu Depurar, escolha Debug/Attach to process…e anexe a outro devenv processo 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 expressões interativamente na segunda instância (com IntelliSense completo e outros recursos).

Você pode desabilitar a depuração just my code para identificar melhor os erros no código gerado. Para obter informações sobre como habilitar ou desabilitar esse recurso, consulte 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 Debug menu e escolhendo Exceptions ou escolhendo as teclas Ctrl+Alt+E para abrir a Exceptions caixa de diálogo. Nessa caixa de diálogo, em Common Language Runtime Exceptions, selecione a opção Thrown.

Implementação do Provedor de Tipos

Esta seção guia você pelas seções principais da implementação do fornecedor de tipos. 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 um projeto F# separado fizer referência ao assembly que contém o tipo. O parâmetro de configuração é 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 TypeProviderForNamespaces tipo da ProvidedTypes API como um tipo base. Esse tipo auxiliar pode fornecer uma coleção finita de namespaces fornecidos ansiosamente, cada um dos quais contém diretamente um número finito de tipos fixos e ansiosamente fornecidos. Nesse contexto, o provedor gera ansiosamente tipos mesmo que eles 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 com mais detalhes 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 ]

Em seguida, 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 makeOneProvidedType função 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 para a propriedade usa uma citação F#, que representa o código que o compilador hospedeiro gera para acessar a propriedade. Para obter mais informações sobre citações, consulte 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 correspondente. Você deve anexar um membro fornecido a apenas um tipo. Caso contrário, o membro nunca será 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 no construtor retorna uma citação F#, que representa o código que o compilador 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 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 InvokeCode do construtor novamente retorna uma citação 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". Você talvez 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 de 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 GetterCode propriedade retorna uma citação F# que especifica o código que o compilador de host gera para obter a propriedade. Da mesma forma que 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 oferece 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 .NET reais para o espaço de informações.

Neste exemplo, cada tipo fornecido é convertido em 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 do provedor pode definir seu próprio tipo de representação privada para ajudar a proteger contra falsas representações. Você não pode definir tipos apagados no próprio F#. Somente os 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?

  • A eliminação 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 dado eliminado são chamados de representações desse tipo. No exemplo neste documento, as representações de todos os tipos Type1..Type100 fornecidos apagados são sempre objetos de string.

Todas as representações de um tipo fornecido devem ser compatíveis com a eliminação do tipo fornecido. (Caso contrário, o compilador F# fornecerá um erro para o uso do provedor de tipos ou o código .NET inverificável que não é válido será gerado. Um provedor de tipos não será válido se retornar um 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 se utilizar a versão com tipagem forte.

  • 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. Frequentemente, 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 em seu 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 construir instâncias desse tipo de objeto:

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

Nesse caso, você pode (opcionalmente) usar esse tipo como a eliminação de tipo especificando esse tipo como o baseType ao construir o 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 em 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 do .NET Regex em uma interface que fornece as seguintes garantias de tempo de compilação:

  • Verificando 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 tipo para criar um RegexTyped tipo que o padrão de expressão regular parametriza para 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 utilizando propriedades nomeadas associadas às correspondências. Ao criar um provedor de tipos, você deve considerar a aparência da API exposta para os usuários finais e como esse design será traduzido para o código .NET. O exemplo a seguir 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 exemplo a seguir mostra como o provedor de tipos traduz 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 Match método 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, consulte 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 tipos usa dois parâmetros estáticos: o pattern, que é obrigatório, e o 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 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 para um objeto fornecido.

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

  • A chamada para o Regex construtor lança um ArgumentException quando uma expressão regular não é válida. O compilador captura essa exceção e relata uma mensagem de erro para o usuário em tempo de 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 significativos. 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 um bool. A única parte complicada é o uso do args argumento dentro da InvokeCode definição. 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 this argumento. No entanto, para um método estático, os argumentos são todos apenas os argumentos explícitos para o método. Observe que o tipo do valor entre aspas deve corresponder ao tipo de retorno especificado (nesse caso, bool). Observe também que esse código usa o AddXmlDoc método para garantir que o método fornecido também tenha 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, esse tipo 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 GetterCode função for fornecida e a propriedade poderá ser gravada se uma SetterCode função for fornecida, de modo que a propriedade resultante seja somente leitura.

Agora você pode criar um método de instância que retorna um valor desse 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

Como você está criando um método de instância, args[0] representa a RegexTyped instância 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 código a seguir é concluído 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 em seus parâmetros estáticos. O provedor verifica o parâmetro estático e fornece operações com base em seu valor.

Um provedor de tipos que é apoiado por dados locais

Frequentemente, talvez você queira 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 tipo 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írgula). Esta seção pressupõe que os arquivos CSV contêm uma linha de cabeçalho seguida por dados de ponto flutuante, como ilustra a tabela a seguir:

Distância (metros) Tempo (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 Distance propriedade do tipo float<meter> e uma Time propriedade 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 SI (Sistema Internacional) conforme o módulo FSharp.Data.UnitSystems.SI.UnitNames Module (F#) define.

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

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

Um provedor mais completo afrouxaria essas restrições.

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

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 exemplo a seguir:

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á colunas diferentes como tendo diferentes tipos de medida. Por exemplo, a primeira coluna em nosso exemplo tem tipo float<meter>e a segunda tem float<second>. No entanto, a representação apagada pode permanecer bastante simples.

O código a seguir mostra o núcleo da implementação.

// 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 um que tenha um esquema idêntico seja lido. Esse padrão é comum quando você escreve um provedor de tipos para fontes de dados locais ou remotas e esse padrão permite que um arquivo local seja usado como o modelo para dados remotos.

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

  • Você pode usar o AddDefinitionLocation método 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 ProvidedMeasureBuilder tipo para pesquisar as unidades 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.

Indo mais longe

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

Uma olhada no código compilado para tipos apagados

Para ter uma ideia de como o uso do provedor de tipos corresponde ao código emitido, examine a função a seguir usando o HelloWorldTypeProvider que é usado 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 InstanceProperty propriedade foram apagadas, deixando apenas operações nos tipos de runtime envolvidos.

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

Siga as convenções a seguir ao criar provedores de tipos.

Provedores de protocolos de conectividade Em geral, os nomes da maioria das DLLs do provedor para protocolos de conectividade de dados e serviço, como conexões OData ou SQL, devem terminar em TypeProvider ou TypeProviders. Por exemplo, use um nome 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 que você implementou:

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

Provedores de utilitário para codificação geral. Para um provedor de tipos de utilitários, como o de expressões regulares, o provedor de tipos pode ser parte de uma biblioteca básica, como mostra o exemplo a seguir:

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

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

  open Fabrikam.Core.Text.RegexTyped

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

Fontes de dados de instância única Alguns provedores de tipos se conectam a apenas uma fonte de dados dedicada e fornecem apenas dados. Nesse caso, você deve remover o TypeProvider sufixo 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, consulte a GetConnection convenção de design 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 tipo.

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 respaldado por dados e serviços remotos, você deve considerar uma série de questões 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 tipo, você poderá querer utilizar as técnicas adicionais a seguir.

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. Consulte a documentação do SDK do Provedor de Tipos para obter mais detalhes.

Fornecimento de anotações relacionadas às unidades 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 fornecedor de tipos pode receber um TypeProviderConfig valor durante o processo de 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 de linguagem F# de que as suposições de esquema podem ter sido alteradas. Quando ocorre a invalidação, uma verificação de tipo será refeita se o provedor estiver 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 de 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 ForceUpdate estático 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 .dll de suporte para 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 backup .dll não é copiado e, 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 tipo gerar uma exceção, o compilador de host atribui o erro a um provedor de tipo 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#, um ambiente de desenvolvimento F# ou 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 lançar uma exceção, você pode lançar os seguintes exemplos: System.NotSupportedException, , System.IO.IOExceptionSystem.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 instruções a seguir devem ser verdadeiras para uma definição de tipo gerada:

  • isErased deve ser definido como false.

  • O tipo gerado deve ser adicionado a um recém-construído ProvidedAssembly(), 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 tipo, tenha em mente as seguintes regras e limitações.

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 T1, T2, T3, ... tipos. 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 dos tipos de 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 do 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 tipo 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 tipo usando as seguintes ferramentas:

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

  • fsi.exe (o compilador interativo F#)

  • devenv.exe (Visual Studio)

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

devenv /debugexe fsc.exe script.fsx

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

Consulte também