Skip to content
This repository has been archived by the owner on May 17, 2019. It is now read-only.

Deseo inyectar una clase de manera Singleton pero esta clase ocupa un contexto que por defecto es Scoped. He investigado un poco pero no me queda claro aún #8

Closed
antonioortizpola opened this issue Apr 20, 2018 · 4 comments
Assignees
Labels
question Further information is requested

Comments

@antonioortizpola
Copy link
Owner

Daniel Aguilar pregunta:

Deseo inyectar una clase de manera Singleton pero ésta clase ocupa un conexto que por defecto es Scope. He investigado un poco pero no me queda claro aún. Cual crees que pueda ser la mejor manera de hacerlo?

@antonioortizpola antonioortizpola added the question Further information is requested label Apr 20, 2018
@antonioortizpola antonioortizpola self-assigned this Apr 20, 2018
@antonioortizpola
Copy link
Owner Author

antonioortizpola commented Apr 20, 2018

Deseo inyectar una clase de manera Singleton pero esta clase ocupa un contexto que por defecto es Scoped

En primer lugar hay que entender el ciclo de vida de los objetos que te da la inyección de dependencias.

Los ciclos Singleton y Transient son fáciles de razonar, ya que son independientes del tipo de la aplicación (si fuera consola, web con mvc o algo más).

Cada que pidas un Transient se va a crear un objeto nuevo y se va a inyectar, pero, ¿qué pasa si inyecto un transient en un singleton?, pues se va a crear un objeto nuevo para ese singleton, sin embargo, como el ciclo de vida de ese objeto es existir mientras la aplicación exista, siempre que acceda a ese objeto será el mismo, mientras que si otros transient lo piden, si se generará un objeto nuevo.

picture1

Eso quiere decir que siempre que accedas a la instancia transient dentro de tu singleton, será la misma que se creó al principio.

Sin embargo, cuando el ciclo de vida es Scoped, quiere decir que su ciclo de vida depende del contexto en el que está. En una aplicación web mvc, este ciclo de vida por defecto es crear una instancia cada request, sin embargo, el inyectarlo en un singleton causaría que esa instancia sea creada solo una vez.

Imagina que guardas los datos del usuario en un scoped para que se carguen una vez al inicio del request (lees el token, checas la firma, validas con la base de datos, y luego cargas los datos del usuario en el objeto), luego si otra instancia ocupa esos datos, simplemente puede inyectar este objeto para utilizarlo:

class SessionData
{
  public User User { get; }

  public SessionData(IHttpContext httpContext, DbContext dbContext) {
    // con httpContext sacas la cookie, validas, etc.
    var userId = GetUserFromCoockie(httpContext);
    User = dbContext.User(userId);
  }
}

Siempre y cuando inyectes esa clase en otro transient o scoped, no hay tanto problema, pero, ¿qué pasa si lo inyectas en un singleton? no sabes con que http context cargó el usuario, y solo lo hiciste una vez, ¿qué pasa si llega el request de otro usuario? Entonces no se puede tener el ciclo Transient dentro de un Singleton, o dará problemas de este tipo.

Cual crees que pueda ser la mejor manera de hacerlo?

Pues como todo en programación... depende :s ....

Cuando hablas de un contexto y un singleton, lo más seguro es que quieres hacer un caché para no estar consultado los datos a la base o algo similar, en ese caso la respuesta es... depende....

Cuando quieres hacer un caché, tienes varias opciones, por ejemplo, puedes guardar las cosas por un tiempo determinado (o inluso dejar para siempre) usando la interfaz IMemoryCache, esta tiene la ventaja de que manejará los problemas de concurrencia por ti, su ejemplo es claro:

El siguiente código usa TryGetValue para revisar si el tiempo está en el caché. Si el tiempo no está en el caché, una entrada nueva es creada y agregada.

public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

Este caché es una muy buena ventaja, además de que puedes usar el código que ya está hecho por ti :D. Tiene opciones como usarlo en forma distribuida (si un día despliegas tu aplicación en un cluster de servidores) y también puedes poner en caché vistas y componentes.

Si se acomoda a tus necesidades úsala y mete el caché en tu capa de persistencia, antes de consultar revisa si ya está el valor como en el ejemplo; sin embargo tiene sus detalles, ya que debes definir un tiempo de vida para el caché, pero ¿qué pasa si necesitas que una instancia se actualice en el código en cuanto cambian su valor?.

En ese caso tienes un problema de concurrencia, ya que tendrás muchos clientes leyendo tu caché y de repente puede entrar un update que cambie los valores a mitad del método de lectura de otros.

En ese caso si, puedes usar un singleton para controlar este acceso, un ejemplo de lo que te comento:

En una aplicación que corre en un solo servidor, tenemos una serie de llaves/valores que guardamos en una base de datos, accedemos mucho a estas llaves y se modifican muy poco, sin embargo cuando alguien cambie un valor, es prioritario que inmediatamente se vea reflejado en nuestros clientes, para lograrlo agregamos un singleton que incluye un candado de lectura/escritura, para asegurarnos que cuando alguien escribe, nadie pueda leer los datos:

namespace Xxx.Services
{
	public class SettingsManager
	{
		// Read/write lock de microsoft
		private readonly ReaderWriterLockSlim _rwLock;
		private readonly DbContextOptions _dbOptions;
		private IDictionary<string, string> _currentSettings;

		// Ya que el Context lo tenemos como transient,
		// para manejar este caso especial donde no hay request
		// en lugar de pedir el contexto directamente pedimos
		// las opciones de configuración, que no nos transient!
		public SettingsManager(DbContextOptions dbOptions)
		{
			_dbOptions = dbOptions;
			_rwLock = new ReaderWriterLockSlim();
			ReloadSettings().GetAwaiter().GetResult();
		}

		public async Task ReloadSettings()
		{
			IDictionary<string, string> newSettings;
			// Donde necesitamos el contexto en lugar de usar DI, usamos las
			// opciones que nos da DI pero creamos el contexto nosotros
			using (var context = new CoreContext(_dbOptions))
			{
				newSettings = await context.Settings.ToDictionaryAsync(x => x.Key, y => y.Value);
			}

			_rwLock.EnterWriteLock();
			_currentSettings = newSettings;
			_rwLock.ExitWriteLock();
		}

		public bool GetSeeting(string key, out string defaultValue)
		{
			_rwLock.EnterReadLock();
			try
			{
				if (_currentSettings.TryGetValue(key, out var setting))
				{
					defaultValue = setting;
					return true;
				}

				defaultValue = null;
				return false;
			}
			finally
			{
				_rwLock.ExitReadLock();
			}
		}

		public string GetSetting(string key)
		{
			return GetSeeting(key, out var setting)
				? setting
				: throw new ArgumentException($"Key '{key}' not found", nameof(key));
		}
	}
}

Más información en los links, o

@antonioortizpola antonioortizpola changed the title Deseo inyectar una clase de manera Singleton pero ésta clase ocupa un conexto que por defecto es Scope. He investigado un poco pero no me queda claro aún Deseo inyectar una clase de manera Singleton pero esta clase ocupa un contexto que por defecto es Scoped. He investigado un poco pero no me queda claro aún Apr 20, 2018
@spk27
Copy link
Collaborator

spk27 commented Apr 20, 2018

Seguro IMemoryCache se adaptaría a mis necesidades, buena opción!.. Sin embargo por lo pronto intento inyectar las opciones del contexto y a diferencia del ultimo ejemplo donde declarabas: private readonly DbContextOptions _dbOptions; en mi caso me obliga a pasarle el tipo de contexto a usar: private readonly DbContextOptions<ContextoBd> _dbContextOptions; ya que en mi ServiceProvider tengo otro contexto para otros propósitos. Pero obtengo la misma excepcion de Ef core por el motivo que no puede consumir un Scope dentro de un Singleton:
image

Clase Singleton:

public class Cache
{
    private readonly ILogger _logger;
    private readonly DbContextOptions<ContextoBd> _dbContextOptions; 

    // objects declarations

    public Cache(ILogger<Cache> logger, DbContextOptions<ContextoBd> dbContextOptions)
    {
        _logger = logger;
        _dbContextOptions = dbContextOptions;
        Update();
    }
    public void Update()
    {
        using (var context = new ContextoBd(_dbContextOptions))
        {
            // some stuff queying and logging
        }
    }
}

Startup:

   public void ConfigureServices(IServiceCollection services)
    {
       services.AddLogging();

        services.AddScoped<Facturar>();
        
        services.AddSingleton<Cache>();

        services.AddTransient<OnChange>();

       services.AddDbContext<ContextoBd>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("ConexionPorDefecto"),
                b => b.MigrationsAssembly("Facturador")));

        services.AddDbContext<BDContexto>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("ConexionPorDefecto"),
                b => b.MigrationsAssembly("Facturador")));


        //some stuff....
       }

Me estaré saltando algo? :/

@antonioortizpola
Copy link
Owner Author

Mmmm, después de estar revisando... hablando de lo rápido que cambia la tecnología :s...

De acuerdo a este issue ya movieron DbContextOptions a scoped, también veo un poco de información aquí.

  • Add another parameter to AddDbContext to allow the options to be registered as Singleton if needed
  • This can be used for apps that are doing some heavy lifting in AddDbContext

Puedes buscar el parámetro dentro del método AddDbContext para agregar las opciones como singleton, ya que tu no necesitas cambiar los datos de conexión entre requests, otra opción un poco más pesada es crear un Scope dentro del singleton para simular el ciclo de vida, pero me parece un poco exagerado.

using (var scope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()
{
    var context = scope.ServiceProvider.GetRequiredService<SchemaContext>())
    // Do stuff
}

La última y menos recomendable opción es apagar este warning de Core, ya que aunque el contexto es scoped, como no se necesita nada del request podrías forzarlo a portarse como transient, pero puedes dispararte en el pie después.

No tuve oportunidad de revisar bien las posibilidades por que ando en la chamba, pero en la noche le doy una revisada, justo vamos a tener este mismo problema en uno de nuestros sistemas ahora que actualicemos, jajajajaja.

@spk27
Copy link
Collaborator

spk27 commented Apr 20, 2018

Había sospechado eso jajaja.. ya que en la clase del curso fer me indicó una solución similar. A fin de cuenta es un detalle de versiones... está interesante como solucionarlo sin hacer mucho ruido pero ntp no hay apuro! thx!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants