Platform Specific no .NET
Escrever código específico de plataforma no .NET é necessário quando uma parte do sistema só funciona no Windows, no macOS ou no Linux. O .NET roda em todos esses sistemas, mas você sabe como isolar o trecho que depende de um deles?
Neste artigo, você vai conhecer a classe OperatingSystem, o atributo SupportedOSPlatform e o analisador que avisa quando uma chamada pode quebrar em outro sistema. Todos os exemplos usam o .NET 6.
Por que escrever código específico de plataforma
O .NET 6 amplia o leque de plataformas suportadas e passa a contar com iOS, Android, Mac Catalyst e muito mais. Isso nos permite criar sistemas para múltiplas plataformas com o mesmo codebase.
Na maior parte do tempo, você nem precisa alterar o código para mudar de sistema operacional. Afinal, APIs como File, HttpClient e Console funcionam da mesma forma em todos eles.
Porém, em alguns casos queremos executar determinadas ações somente em uma plataforma, ou em um grupo delas. Por exemplo, ler o registro do Windows, chamar uma API nativa via P/Invoke ou acessar um recurso exclusivo do macOS.
Nesses cenários, o código específico de plataforma deixa de ser exceção e vira uma necessidade. Por isso, o .NET oferece ferramentas próprias para verificar o sistema atual e para documentar restrições.
Se você quer ter uma visão geral de tudo o que dá para construir com a plataforma, vale ler sobre o ecossistema .NET.
Código específico de plataforma com OperatingSystem
A forma mais simples de executar código específico de plataforma é perguntar ao runtime em qual sistema operacional ele está rodando. Para isso, temos a classe OperatingSystem, que expõe métodos estáticos para cada plataforma.
if (OperatingSystem.IsWindows())
Console.WriteLine("Running on Windows");
if (OperatingSystem.IsMacOS())
Console.WriteLine("Running on macOS");
if (OperatingSystem.IsLinux())
Console.WriteLine("Running on Linux");
Vale um detalhe: OperatingSystem é uma classe, e não um enumerador. Os métodos Is<Plataforma>() chegaram no .NET 5 e retornam um bool com a resposta.
A classe cobre todas as plataformas suportadas pelo .NET. Além disso, quando novas plataformas forem adicionadas, elas também aparecerão aqui.
Métodos disponíveis no .NET 6
No .NET 6, você encontra métodos como IsWindows(), IsLinux(), IsMacOS(), IsAndroid(), IsIOS(), IsMacCatalyst(), IsTvOS(), IsWatchOS(), IsBrowser() e IsFreeBSD(). Também existe o IsOSPlatform(string), que recebe o nome da plataforma como texto.
Um ponto importante: o IsIOS() retorna true também no Mac Catalyst. Isso acontece porque o .NET 6 introduziu o conceito de inclusão de plataformas, em que o Mac Catalyst funciona como um superconjunto do iOS.
Verificando a versão do sistema
Muitas vezes, não basta saber qual é o sistema, e sim qual é a versão dele. Para esse caso, a classe oferece métodos como IsWindowsVersionAtLeast e IsAndroidVersionAtLeast.
// Executa apenas no Windows 10 build 17763 ou superior
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763))
{
Console.WriteLine("Windows 10 1809 or later");
}
Dessa forma, você pode aproveitar recursos novos do sistema operacional sem quebrar a aplicação em versões antigas.
Diferença para RuntimeInformation
Antes do .NET 5, a forma comum de verificar o sistema era RuntimeInformation.IsOSPlatform(OSPlatform.Windows). Essa API ainda funciona e continua útil em projetos que miram o .NET Standard.
No entanto, a documentação oficial informa que ela não é tão otimizada quanto os métodos de OperatingSystem. Portanto, em projetos .NET 5 ou superior, prefira a classe nova.
Marcando código específico de plataforma com SupportedOSPlatform
Verificar o sistema em tempo de execução resolve metade do problema. A outra metade é avisar quem consome o seu código de que um método não funciona em todos os lugares.
Para isso, temos o atributo SupportedOSPlatform, do namespace System.Runtime.Versioning. Com ele, você declara que um método, uma classe ou um assembly inteiro só roda em determinadas plataformas.
using System.Runtime.Versioning;
public class CustomFileWriter
{
[SupportedOSPlatform("windows")]
public void WindowsOnly()
{
// Código que depende de APIs exclusivas do Windows
}
}
Em seguida, o compilador passa a emitir um warning sempre que alguém chamar esse método a partir de um contexto que roda em qualquer plataforma. Assim, o código específico de plataforma fica documentado no próprio código.
O aviso CA1416
Imagine que você está no Mac e chama o método WindowsOnly() de um projeto multiplataforma. Nesse caso, o build exibe o seguinte aviso:
warning CA1416: This call site is reachable on all platforms. 'CustomFileWriter.WindowsOnly()' is only supported on: 'windows'.
Esse aviso vem do analisador de compatibilidade de plataforma, uma das regras de qualidade do Roslyn incluídas no SDK. Segundo a documentação da regra CA1416, ela fica ativa por padrão em projetos que miram o .NET 5 ou superior.
Além disso, IDEs como o JetBrains Rider e o Visual Studio interpretam esse alerta enquanto você digita. Ou seja, você descobre o problema antes mesmo de compilar.
Suportando múltiplas plataformas
O atributo SupportedOSPlatform é cumulativo. Isso significa que você pode aplicá-lo várias vezes ao mesmo membro para indicar suporte a mais de uma plataforma.
using System.Runtime.Versioning;
public class CustomFileWriter
{
[SupportedOSPlatform("windows")]
public void WindowsOnly()
{
}
[SupportedOSPlatform("macos")]
public void MacOnly()
{
}
[SupportedOSPlatform("windows"), SupportedOSPlatform("macos")]
public void WindowsAndMac()
{
}
}
Infelizmente, não existe um enumerador com o nome das plataformas. Por isso, você precisa informá-las como string, o que abre espaço para erros de digitação.
Uma boa prática é centralizar esses nomes em constantes da sua aplicação. Assim, o código específico de plataforma fica mais fácil de revisar e de manter.
Informando a versão mínima
O nome da plataforma também pode incluir uma versão. Por exemplo, [SupportedOSPlatform("windows10.0.18362")] indica que o método exige pelo menos aquela build do Windows 10.
Nesse caso, o guarda correspondente precisa verificar uma versão igual ou superior. Ou seja, um if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 18362)) satisfaz o analisador.
O atributo UnsupportedOSPlatform
Às vezes, a lógica é inversa: o método funciona em quase todos os sistemas, exceto em um. Para esse cenário, existe o atributo UnsupportedOSPlatform.
using System.Runtime.Versioning;
public class MediaService
{
// Funciona em todas as plataformas, exceto no navegador
[UnsupportedOSPlatform("browser")]
public void PlayLocalFile(string path)
{
}
}
Assim, o analisador avisa apenas quando a chamada acontece em um contexto que pode rodar na plataforma marcada como não suportada.
Você também pode combinar os dois atributos no mesmo membro. Por exemplo, marcar um método como não suportado no Windows e, ao mesmo tempo, suportado a partir de uma versão específica dele.
Nesse caso, o analisador agrupa os atributos por plataforma e olha para a menor versão de cada uma. Se a menor versão vem de um SupportedOSPlatform, a lista funciona como permissão, ou seja, só as plataformas listadas são suportadas.
Por outro lado, se a menor versão vem de um UnsupportedOSPlatform, a lista funciona como bloqueio. Assim, todas as plataformas são suportadas, exceto as que aparecem ali, o que ajuda a modelar APIs que ganharam suporte em versões mais novas de um sistema.
Como resolver o aviso no código específico de plataforma
Quando o CA1416 aparece, a documentação oficial lista algumas saídas. A melhor delas depende do contexto do seu código específico de plataforma.
Protegendo a chamada com um guarda
A abordagem recomendada é envolver a chamada com um método de verificação de OperatingSystem. O analisador reconhece esses métodos como guardas e deixa de emitir o aviso.
var writer = new CustomFileWriter();
if (OperatingSystem.IsWindows())
{
writer.WindowsOnly(); // Sem aviso, a chamada está protegida
}
Com isso, a aplicação continua multiplataforma e o trecho exclusivo só roda quando faz sentido.
Propagando a restrição para quem chama
Outra opção é marcar o método chamador com o mesmo atributo. Dessa forma, a restrição sobe na cadeia de chamadas e o aviso passa a aparecer só para quem consome esse método.
public class ReportExporter
{
[SupportedOSPlatform("windows")]
public void Export()
{
// Contexto já marcado como Windows, então não há aviso
new CustomFileWriter().WindowsOnly();
}
}
Você pode aplicar o atributo em tipos, membros e assemblies. Quando ele está no assembly ou na classe, todos os membros internos herdam a restrição.
Usando Debug.Assert
Se você não quer o custo de um if extra em tempo de execução, pode usar Debug.Assert com a verificação de plataforma. O analisador também respeita essa forma.
using System.Diagnostics;
Debug.Assert(OperatingSystem.IsWindows());
new CustomFileWriter().WindowsOnly(); // Sem aviso
Contudo, lembre-se de que o Debug.Assert some nas builds de Release. Portanto, use essa opção apenas quando você tem certeza de que o fluxo nunca chega ali em outro sistema.
Suprimindo o aviso
Por fim, você pode suprimir o aviso com #pragma warning disable CA1416 ou por meio de um arquivo .editorconfig. No entanto, a documentação trata essa saída como último recurso.
Afinal, suprimir o aviso esconde um risco real de falha em produção. Se precisar fazer isso, deixe um comentário explicando o motivo.
Guardas personalizados para código específico de plataforma
O .NET 6 trouxe dois atributos novos para quem quer criar seus próprios guardas: SupportedOSPlatformGuard e UnsupportedOSPlatformGuard. Com eles, você ensina o analisador a reconhecer um campo, uma propriedade ou um método como verificação válida.
Isso é útil quando você quer guardar o resultado da verificação em um campo e reaproveitá-lo em vários pontos do código específico de plataforma.
using System.Runtime.Versioning;
public class DesktopIntegration
{
// O analisador passa a reconhecer este campo como guarda
[SupportedOSPlatformGuard("windows")]
[SupportedOSPlatformGuard("macos")]
private readonly bool _isDesktop = OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
public void Run()
{
if (_isDesktop)
{
new CustomFileWriter().WindowsAndMac(); // Sem aviso
}
}
}
Da mesma forma, UnsupportedOSPlatformGuard marca verificações negativas, como uma propriedade que retorna !OperatingSystem.IsBrowser().
Projetos inteiros com código específico de plataforma
Existe ainda uma forma de declarar que um projeto inteiro é específico de uma plataforma. Para isso, você usa um target framework com sufixo, como net6.0-windows.
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
</PropertyGroup>
Nesse caso, o SDK injeta automaticamente um SupportedOSPlatform("windows") no assembly. Assim, chamadas a APIs exclusivas do Windows dentro desse projeto não geram avisos.
Esse modelo combina bem com projetos multi-target, em que cada target framework compila uma versão diferente do código. Projetos .NET MAUI, por exemplo, usam esse recurso para gerar builds de Android, iOS, macOS e Windows, como mostra o artigo sobre .NET MAUI no desktop com Windows e macOS.
Código específico de plataforma em tempo de compilação
Até aqui, todas as verificações aconteceram em tempo de execução. Contudo, também é possível excluir um trecho do binário final com diretivas de pré-processador.
Quando o projeto usa um target framework com sufixo de sistema operacional, o SDK define símbolos como WINDOWS, ANDROID, IOS, MACCATALYST, MACOS e TVOS. Além disso, existem símbolos versionados, como WINDOWS10_0_19041_0_OR_GREATER.
public static class PlatformInfo
{
public static string GetName()
{
#if WINDOWS
// Compilado apenas no target net6.0-windows
return "Windows build";
#elif ANDROID
// Compilado apenas no target net6.0-android
return "Android build";
#else
return "Cross-platform build";
#endif
}
}
Nesse modelo, o código específico de plataforma nem chega aos outros binários. Por outro lado, ele exige multi-targeting, porque cada target framework gera uma compilação separada.
Em geral, a verificação em tempo de execução é mais simples para aplicações de console e APIs. Já as diretivas fazem mais sentido em bibliotecas e aplicações MAUI, que já compilam um binário por plataforma.
Boas práticas para código específico de plataforma
Antes de escrever um trecho exclusivo, verifique se já existe uma alternativa multiplataforma no próprio .NET. Muitas vezes, uma API portável resolve o problema sem nenhuma condição.
Quando o código específico de plataforma for inevitável, isole-o atrás de uma interface. Por exemplo, crie uma INotificationService com uma implementação para Windows e outra para macOS, escolhida na injeção de dependência.
public interface INotificationService
{
void Notify(string message);
}
[SupportedOSPlatform("windows")]
public class WindowsNotificationService : INotificationService
{
public void Notify(string message)
{
// Implementação baseada em APIs do Windows
}
}
Além disso, trate os avisos CA1416 como erros em projetos críticos. Dessa forma, nenhuma chamada desprotegida chega à branch principal.
Se você desenvolve no Linux, também vale entender como está o suporte da ferramenta nesse sistema, tema do artigo é viável programar em C# no Linux.
Testando código específico de plataforma
Testes automatizados também precisam lidar com o sistema operacional. Afinal, um teste que chama uma API exclusiva do Windows vai falhar em um agente de CI com Linux.
A solução mais simples é aplicar a mesma lógica de guarda dentro do teste. Assim, o teste só executa a parte exclusiva quando o sistema corresponde ao esperado.
public class CustomFileWriterTests
{
[Fact]
public void WindowsOnly_ShouldRun_OnWindows()
{
// Ignora a verificação fora do Windows
if (!OperatingSystem.IsWindows())
return;
var writer = new CustomFileWriter();
writer.WindowsOnly();
}
}
Além disso, vale rodar a suíte em uma matriz de sistemas operacionais no pipeline de CI. Dessa forma, você garante que cada trecho de código específico de plataforma foi exercitado no ambiente certo.
Outra vantagem da abordagem com interfaces aparece aqui. Como cada implementação fica isolada, você consegue usar um dublê de teste no lugar dela e validar o restante da lógica em qualquer sistema.
Conclusão
O .NET suporta múltiplas plataformas, e a maior parte do código pode ser escrita de forma genérica. Ainda assim, alguns pontos precisam ser exclusivos de um sistema operacional.
Nesses casos, use OperatingSystem para verificar o sistema em tempo de execução e SupportedOSPlatform para documentar as restrições. Com isso, o seu código específico de plataforma fica seguro, explícito e fácil de manter.
FAQ
OperatingSystem é um enum?
Não. OperatingSystem é uma classe com métodos estáticos como IsWindows() e IsLinux(), que retornam um valor booleano.
O que significa o aviso CA1416?
Ele indica que você chamou uma API específica de uma plataforma a partir de um código que pode rodar em outras. A correção recomendada é proteger a chamada com uma verificação de OperatingSystem.
Posso aplicar SupportedOSPlatform em uma classe inteira?
Sim. O atributo funciona em assemblies, tipos e membros, e todos os membros internos herdam a restrição.
Qual a diferença entre OperatingSystem e RuntimeInformation?
Os dois verificam o sistema, mas OperatingSystem é mais otimizado e existe a partir do .NET 5. Já RuntimeInformation.IsOSPlatform continua útil em bibliotecas .NET Standard.
O IsIOS retorna true no Mac Catalyst?
Sim. A partir do .NET 6, o Mac Catalyst é tratado como superconjunto do iOS, então IsIOS() também retorna true nele.