Instalador .NET com a connection string do seu PC
O MSI levou a connection string do seu PC para o cliente? Prepare o app .NET antes: configuração no primeiro uso no AppData e senha protegida com DPAPI.
"Não foi possível conectar ao servidor NOTE-DEV01." A mensagem aparece na tela do cliente no primeiro clique depois da instalação, e NOTE-DEV01 é o notebook de quem desenvolveu o sistema. O MSI fez exatamente o que devia: copiou a saída do build, e a saída do build tinha a connection string de desenvolvimento dentro do appsettings.json. O problema não está no instalador, e sim no aplicativo, que ainda não sabe viver fora da máquina onde nasceu.
Este post mostra o que mudar no código de um programa de desktop .NET antes de gerar o MSI: configuração criada no primeiro uso, senha protegida pelo Windows e nada de configuração dentro da pasta do programa. O código compila com o SDK do .NET 10. A parte de implantação (runtime, firewall do SQL Server, teste em máquina limpa) está em instalador .NET: o que o setup não faz no cliente.
Três sinais de que o app não está pronto para instalar
- Connection string no arquivo de configuração do build. Tudo o que está em
appsettings.jsonouApp.configvai igual para todos os clientes, com o servidor, o usuário e muitas vezes a senha de quem desenvolveu. - O programa grava ao lado do
.exe. Instalado emC:\Program Files, ele não tem permissão de escrita ali quando roda como usuário comum. No seu computador funciona porque você é administrador ou roda pela pasta do projeto. - Senha em texto puro. Qualquer pessoa que abrir o arquivo de configuração vê a senha do banco da loja.
Configuração do cliente, criada no primeiro uso
A configuração de cada loja (servidor, banco, usuário, senha) não pode vir do build. Ela é perguntada na primeira execução e gravada numa pasta do usuário. O Environment.SpecialFolder.LocalApplicationData aponta para C:\Users\<usuário>\AppData\Local, onde o programa sempre pode escrever e que o MSI não apaga numa atualização, porque não foi ele que criou o arquivo.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Data.SqlClient;
namespace CaixaFacil;
// Configuração da loja, criada no primeiro uso e guardada fora da pasta do programa.
public sealed class ConfigLocal
{
public string Servidor { get; set; } = ""; // ex.: ESCRITORIO-01\SQLEXPRESS
public string Banco { get; set; } = "CaixaFacil";
public string Usuario { get; set; } = "";
public string SenhaProtegida { get; set; } = ""; // DPAPI, em Base64
// Pequena "entropia" própria do app: outro programa do mesmo usuário
// não consegue abrir a senha só chamando Unprotect.
private static readonly byte[] Entropia = Encoding.UTF8.GetBytes("CaixaFacil.config.v1");
public static string Caminho => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"CaixaFacil", "config.json");
public static ConfigLocal? Carregar()
{
if (!File.Exists(Caminho)) return null; // primeiro uso: abrir a tela de configuração
return JsonSerializer.Deserialize<ConfigLocal>(File.ReadAllText(Caminho));
}
public void Salvar()
{
Directory.CreateDirectory(Path.GetDirectoryName(Caminho)!);
File.WriteAllText(Caminho, JsonSerializer.Serialize(this,
new JsonSerializerOptions { WriteIndented = true }));
}
public void DefinirSenha(string senha) =>
SenhaProtegida = Convert.ToBase64String(ProtectedData.Protect(
Encoding.UTF8.GetBytes(senha), Entropia, DataProtectionScope.CurrentUser));
public string MontarConnectionString()
{
var senha = Encoding.UTF8.GetString(ProtectedData.Unprotect(
Convert.FromBase64String(SenhaProtegida), Entropia, DataProtectionScope.CurrentUser));
return new SqlConnectionStringBuilder
{
DataSource = Servidor,
InitialCatalog = Banco,
UserID = Usuario,
Password = senha,
Encrypt = SqlConnectionEncryptOption.Mandatory,
ConnectTimeout = 5
}.ConnectionString;
}
}
O DataSource recebe o que a pessoa digitou na tela de primeiro uso, como ESCRITORIO-01\SQLEXPRESS. O SqlConnectionStringBuilder do Microsoft.Data.SqlClient (versão 6.1.7 no teste) monta a string sem concatenação, então um ; na senha não quebra nada. A conexão exige criptografia, que é o padrão do SqlClient desde a versão 4: se o SQL Server da loja usa um certificado autoassinado, a conexão vai falhar até alguém instalar um certificado confiável no servidor. Resolver com TrustServerCertificate=true funciona, mas desliga a verificação do certificado.
A senha protegida pelo Windows
O ProtectedData usa a DPAPI, a API de proteção de dados do próprio Windows, e vem no pacote System.Security.Cryptography.ProtectedData (10.0.14 no teste). Não há chave para guardar: o Windows deriva a chave da conta do usuário. O escopo define quem consegue abrir, segundo a documentação do DataProtectionScope:
CurrentUser: só processos rodando com o mesmo usuário do Windows. É o que o exemplo usa.LocalMachine: qualquer processo daquele computador. Serve quando vários usuários do Windows usam o mesmo caixa, mas protege bem menos.
Duas consequências práticas. A senha protegida num computador não abre em outro, então não copie o config.json de um caixa para o outro: cada máquina passa pelo primeiro uso. E a DPAPI só existe no Windows; no Linux, a chamada lança PlatformNotSupportedException.
O ponto de entrada decide entre primeiro uso e caixa
namespace CaixaFacil;
static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
var config = ConfigLocal.Carregar();
if (config is null)
{
using var tela = new TelaPrimeiroUso(); // pede servidor, usuário e senha,
if (tela.ShowDialog() != DialogResult.OK) return; // testa a conexão e chama Salvar()
config = tela.Config;
}
Application.Run(new TelaCaixa(config.MontarConnectionString()));
}
}
// Esqueletos só para o exemplo compilar.
public class TelaPrimeiroUso : Form { public ConfigLocal Config { get; } = new(); }
public class TelaCaixa : Form { public TelaCaixa(string conexao) { Text = "Caixa"; } }
A tela de primeiro uso deve testar a conexão antes de salvar, com uma mensagem que diga o que fazer ("confira o nome do computador do escritório e se ele está ligado"), e não a exceção crua. O mesmo caminho serve para trocar de servidor depois, como em connection string não se troca na tela: apague ou edite o config.json por uma opção de menu protegida, nunca por um campo livre na tela principal.
O que fica no build e o que fica fora
Depois dessa mudança, o appsettings.json que sai do build só tem o que é igual para todos os clientes, como nível de log e endereço do serviço de atualização. A string de desenvolvimento vai para appsettings.Development.json ou para os User Secrets, e você confere que esse arquivo não aparece na pasta publicada. Antes de gerar o MSI, abra a pasta publicada e procure pelo nome do seu computador e pela senha do banco de desenvolvimento. Se aparecer, o instalador vai levar junto.
Com a configuração no AppData e os dados no banco, atualizar é trocar arquivos em Program Files. Mesmo assim, faça o backup do banco antes, como em atualizar o sistema com backup.
O projeto do exemplo (ConfigTeste.csproj, ConfigLocal.cs e Program.cs, mirando net10.0-windows) compilou sem avisos com o SDK 10.0.402. Ele não foi executado, porque a DPAPI exige Windows e o ambiente usado para escrever é Linux.
Referência: https://learn.microsoft.com/pt-br/visualstudio/deployment/installer-projects-net-core?view=vs-2022