22 messaggi dal 06 settembre 2002
Fermo restando che la tecnica dei servizi e della DI integrata in ASP.NET Core e' la soluzione ideale per avere un codice facilmente estendibile, trovo pero' scomodo il dover ricorrere alla DI per la lettura dei valori di Appsettings.

Io vorrei poter leggere i valori di Appsettings (come ad esempio la solita stringa di connessione al db) in qualsiasi punto dell'applicazione senza l'utilizzo della DI.

Percio' girando su internet ho trovato questa semplice soluzione https://eanirudh.wordpress.com/2018/05/31/reading-appsettings-json-in-class-library-net-core-2-0/ che consente di leggere i valori di Appsettings senza bisogno della DI.

Basta definire questa classe statica:

  public static class ConfigHelper
  {
        public static IConfiguration GetConfig()
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(System.AppContext.BaseDirectory)
                .AddJsonFile("appSettings.json", optional: true, reloadOnChange: true);
            return builder.Build();
        }
  } 


e leggere i valori con il seguente codice (che posso inserire dove voglio):

setting = ConfigHelper.GetConfig();
var connectionstring = setting.GetSection("ConnectionStrings").GetSection("DefaultConnection").Value;


Ho fatto delle prove e tutto funziona bene! (quasi!!)

L'unico problema che ho incontrato e' stato che compilando l'applicazione in Visual Studio, il file Appsettings.json non viene copiato nella cartella \bin\Debug\netcoreapp2.1 e quindi la variabile setting e' sempre a null.

Se pero' copio manualmente il file Appsettings.json nella cartella tutto funziona perfettamente.

Facendo il publish dell'applicazione il file Appsettings.json viene invece copiato automaticamente nella radice e quindi non ci dovrebbero essere problemi.

Domanda 1.
Vorrei avere un parere su questa tecnica che a me sembra una buona soluzione.

E' cosi' oppure ci sono delle controindicazioni che non riesco a vedere?

Domanda 2.
Il puntamento al file Appsettings con il metodo

.SetBasePath(System.AppContext.BaseDirectory)


e' corretto o bisognerebbe utilizzare un'altra tecnica?

Domanda 3.
C'e' qualche parametro che prevede la copia automatica del file Appsettings.json nella cartella \bin\Debug\netcoreapp2.1 ?

(un po' come viene fatto in .NET con il file App.config che viene rinominato e copiato automaticamente nella cartella \bin\Debug)

Grazie, salvo
11.645 messaggi dal 09 febbraio 2002
Contributi
Ciao,


trovo pero' scomodo il dover ricorrere alla DI per la lettura dei valori di Appsettings

Puoi spiegare come mai?


Domanda 1.
Vorrei avere un parere su questa tecnica che a me sembra una buona soluzione.

Ci sono due problemi, per i quali io non seguirei mai questo approccio.
  • è inefficiente perché ad ogni invocazione di GetConfig ricostruisci il ConfigurationBuilder;
  • la dipendenza da una classe statica riduce o impedisce del tutto la testabilità dei tuoi componenti.



Domanda 2.
Il puntamento al file Appsettings con il metodo [...] e' corretto o bisognerebbe utilizzare un'altra tecnica?

Ti sconsiglio di creare il tuo ConfigurationBuilder e riusare quello creato da ASP.NET Core, che punta al file appsettings.json situato nella ContentRoot dell'applicazione.

Modifica così il costruttore della classe Startup.
public Startup(IConfiguration configuration)
{
   ConfigHelper.Instance = configuration;
}


Dove Instance è una proprietà statica che dovrai creare nella tua classe ConfigHelper.
public static class ConfigHelper
{
    public static IConfiguration Instance { get; set; }
} 

Questa mia implementazione è puramente dimostrativa e presuppone che la proprietà Instance non venga mai pi riassegnata in nessun altro punto dell'applicazione. Per recuperare la connection string ora puoi fare:
var connString = ConfigHelper.Instance.GetConnectionString("Default");



Domanda 3.
C'e' qualche parametro che prevede la copia automatica del file Appsettings.json nella cartella \bin\Debug\netcoreapp2.1 ?

Il file non viene copiato perché non serve, infatti quando lanci il debug la ContentRoot dell'applicazione è la directory principale del progetto, cioè quella in cui si trova l'appsettings.json.

Se comunque tu vuoi copiarlo nella directory di compilazione, modifica così il tuo file .csproj.
  <ItemGroup>
    <Folder Include="wwwroot\" />
    <None Include="appsettings.json" CopyToOutputDirectory="Always" />
  </ItemGroup>


ciao,
Moreno
Modificato da BrightSoul il 20 ottobre 2018 18.04 -

Enjoy learning and just keep making
22 messaggi dal 06 settembre 2002
Vediamo se riesco a spiegarmi.

Sono ancora un novellino di ASP.NET Core ma da quello che ho capito, registrando una classe come servizio la DI puo' essere utilizzata solo nel costruttore di un Controller, Middleware, View, Hosted Service.

Riprendo un esempio che hai inserito in una risposta nel forum.

Diciamo che ho definito questa Appsettings:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=abca;database=CustomerDB;Password=xyz;User ID=sa"
  },
  "Smtp": {
      "Host": "smtp.example.com",
      "Port": 25,
      "Username": "Pippo",
      "Password": "Puppo",
      "UseSSL": false
      },
  "AltraProprietà": "Whatever",
  ...
}



Definisco la classe SmtpOptions che riflette i valori dell'Appsettings

public class SmtpOptions 
{
    public string Host { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public bool UseSSL { get; set; }
}


Definisco questa interfaccia e la classe per l'invio email:

public interface ISmtpClient {
  Task SendMail(string from, string to, string subject, string body);
}

public class SmtpClient : ISmtpClient
{
    private readonly IOptionsMonitor<SmtpOptions> smtpOptions;

    public SmtpClient(IOptionsMonitor<SmtpOptions> smtpOptions)
    {
       this.smtpOptions = smtpOptions;
    }

    public async Task SendMail(string from, string to, string subject, string body) 
    {
      //Accedo alla configurazione
      //Prima ottengo la configurazione attuale dal monitor
      var config = smtpOptions.CurrentValue; //E poi leggo i singoli valori
      var host = config.Host;
      var port = config.Port;
      var username = config.Username;
      var password = config.Password;
      var useSSL = config.UseSSL;
      //Implementazione di invio email qui, usando i parametri ottenuti dalla configurazione
      ...
    }
}


Infine registro il servizio

public void ConfigureServices(IServiceCollection services)
{
    ...
    //Registro l'SmtpClient come servizio
    services.AddTransient<ISmtpClient, SmtpClient>();
    //Registro la configurazione per SmtpOptions, che mi permettera' di usare il servizio IOptionsMonitor
    services.Configure<SmtpOptions>(Configuration.GetSection("Smtp"));
}


Adesso definisco una Razor Pages per l'invio di email, definisco l'evento OnPost per l'invio e la DI nel costruttore della View:

public class ContactModel : PageModel
{
    private readonly ISmtpClient _smtpClient;

    public ContactModel(ISmtpClient smtpClient)
    {
         _smtpClient = smtpClient;
    }

    [BindProperty] public string From { get; set; }
    [BindProperty] public string To { get; set; }
    [BindProperty] public string Subject { get; set; }
    [BindProperty] public string Email { get; set; }

    public async Task<IActionResult> OnPost()
    {
        await _smtpClient.Send(From, To, Subject, Email);
        return RedirectToPage("Thanks");
    }
}


E fin qui funziona tutto per bene.

Adesso viene fatta una richiesta: registrare nel database una log con l'esito dell'invio email.

A me vengono in mente due soluzioni.

Soluzione 1.

1. creare una classe che registra nel db la log dell'email:

public class LogEmail
{
    private readonly string connectionString = ??

    private readonly string _from;
    private readonly string _to;
    private readonly string _subject;
    private readonly string _email;
    private readonly bool _esito;

    public LogEmail(from, to, subject, email, esito)
    {
       _from = from;
       _to = to;
       _subject = subject;
       _email = email;
       _esito = esito;
    }

    public int Registra()
    {
        // istruzioni per l'insert
        ...
    }
}


2. aggiornare la classe SmtpClient per gestire la registrazione della log:
public class SmtpClient : ISmtpClient
{
    ...
    
    public async Task SendMail(string from, string to, string subject, string body) 
    {
      //Accedo alla configurazione
      //Prima ottengo la configurazione attuale dal monitor
      var config = smtpOptions.CurrentValue; //E poi leggo i singoli valori
      var host = config.Host;
      var port = config.Port;
      var username = config.Username;
      var password = config.Password;
      var useSSL = config.UseSSL;
      //Implementazione di invio email qui, usando i parametri ottenuti dalla configurazione
      ...
      // scrittura della log email
      LogEmail log = new LogEmail(from, to, subject, body, esito);
      log.Registra();
      
    }
}


Domanda.
La classe LogEmail come ottiene la stringa di connessione?

Potrei definire la classe LogEmail come servizio per poter accedere all'Appsettings.

Ma nella classe SmtpClient non posso sfruttare la DI, giusto?
Dovrebbe essere la classe SmtpClient che accede all'Appsettings e passa la stringa di connessione nel costruttore di LogMail.

Soluzione 2.

1. creare una classe che registra nel db la log dell'email (come prima)

2. definire la classe LogEmail come servizio che nel costruttore accede alla configurazione

3. richiamare la scrittura della log nella View ContactModel, sfruttando la DI nel costruttore della classe LogEmail

Osservazione.
Io avrei preferito poter accedere all'Appsettings all'interno della classe senza dipendenze esterne.

Potrei al esempio spostare la classe LogEmail in una libreria separata.

Sperando di essere stato sufficientemente chiaro, che soluzione proporresti?

Grazie
salvo
11.645 messaggi dal 09 febbraio 2002
Contributi

Io avrei preferito poter accedere all'Appsettings all'interno della classe senza dipendenze esterne.
Potrei al esempio spostare la classe LogEmail in una libreria separata.

Non ha importanza: anche se la classe è in una libreria separata, le sue opzioni e le sue dipendenze vanno sempre esplicitate come parametri del costruttore. Se non lo fai, vuol dire che stai cablando in maniera forte una classe ad un'altra classe. Sarebbe come costruire un'automobile il cui motore è un tutt'uno con le candele: quando una delle candele si deteriora, non potrai semplicemente sostituirla ma sarai costretto a sostituire l'intero motore. Se gli ingegneri costruiscono componenti disaccoppiati è per una migliore manutenibilità e longevità del prodotto. Guarda quanti piccoli componenti ha un motore. Un'applicazione dovrebbe seguire lo stesso principio costruttivo.
http://cdn.johnywheels.com/2015/11/25/v8engineexplodedview-l-249214c463669b0c.jpg



La classe LogEmail come ottiene la stringa di connessione?

Ricevendo un servizio IConfiguration dal costruttore, come al solito. Se LogEmail è un servizio registrato con la dependency injection di ASP.NET Core, bene, la dipendenza verrà risolta automaticamente. Altrimenti, vuol dire che qualcuno dovrà fornirla dall'esterno (ad esempio dovrà essere la razor page da cui istanzi il LogEmail a fornirla).

Dovendo aggiungere la funzionalità di logging all'invio email, si potrebbe provare con il decorator pattern ma tu falla più facile: l'SmtpClient prende come dipendenza anche un servizio di logging (qui chiamato IEmailLogger).
public class SmtpClient : ISmtpClient
{
    private readonly IOptionsMonitor<SmtpOptions> smtpOptions;
    private readonly IEmailLogger emailLogger;

    public SmtpClient(IOptionsMonitor<SmtpOptions> smtpOptions, IEmailLogger emailLogger)
    {
       this.smtpOptions = smtpOptions;
       this.emailLogger = emailLogger;
    }

    public async Task SendMail(string from, string to, string subject, string body) 
    {
      //Accedo alla configurazione
      //Prima ottengo la configurazione attuale dal monitor
      var config = smtpOptions.CurrentValue; //E poi leggo i singoli valori
      var host = config.Host;
      var port = config.Port;
      var username = config.Username;
      var password = config.Password;
      var useSSL = config.UseSSL;
      //Implementazione di invio email qui, usando i parametri ottenuti dalla configurazione
 
      //Qui logga l'esito
      await emailLogger.Register(from, to, subject, esito);
      
    }
}


L'interfaccia IEmailLogger sarà implementata così dalla classe LogEmail. Nota come riceva l'IConfiguration dal costruttore.
public class LogEmail : IEmailLogger
{
    private readonly IConfiguration configuration;

    public LogEmail(IConfiguration configuration)
    {
      this.configuration = configuration;
    }

    public async Task<int> Register(string from, string to, string subject, bool esito)
    {
        var connectionString = configuration.GetConnectionString("Default");
        using (var conn = new SqlConnection())
        {
          await conn.OpenAsync();
          //Qui insert
        }
    }
}


E infine registri il servizio IEmailLogger per la dependency injection di ASP.NET Core.
services.AddTransient<IEmailLogger, LogEmail>();


Le dipendenze vengono risolte a cascata, quindi se una classe ha una dipendenza da ISmtpClient e l'implementazione di ISmptClient ha, a sua volta, una dipendenza da IEmailLogger che, a sua volta, ha una dipendenza da IConfiguration, il meccanismo di dependency injection di ASP.NET Core non avrà problemi a risolvere tutto il grafo di dipendenze.

ciao,
Moreno
Modificato da BrightSoul il 21 ottobre 2018 11.30 -

Enjoy learning and just keep making

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.