Acelerar Entity Framework Core

¡No seas avaricioso!


Al seleccionar datos, debe elegir exactamente tantos como necesite a la vez. ¡Nunca recupere todos los datos de una tabla!

Incorrecto:

using var ctx = new EFCoreTestContext(optionsBuilder.Options);                
//    ID  ,       !
ctx.FederalDistricts.Select(x=> new { x.ID, x.Name, x.ShortName }).ToList();

Correctamente:

using var ctx = new EFCoreTestContext(optionsBuilder.Options);  
//     ID     !
ctx.FederalDistricts.Select(x=> new { x.Name, x.ShortName }).ToList();
ctx.FederalDistricts.Select(x => new MyClass { Name = x.Name, ShortName = x.ShortName }).ToList();


Incorrecto:

var blogs = context.Blog.ToList(); //       . ?
//     ?
var somePost = blogs.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));

Correctamente:

var somePost = context.Blog.FirstOrDefault(x=>x.Title.StartWidth(“Hello world!”));

La validación de datos integrada se puede realizar cuando una consulta devuelve algunos registros.

Incorrecto:


var blogs = context.Blogs.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")).ToList();

public static string StandardizeUrl(string url)
{
    url = url.ToLower();
    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }
    return url;
}

Correctamente:

var blogs = context.Blogs.AsEnumerable().Where(blog => StandardizeUrl(blog.Url).Contains("dotnet")).ToList();
 
//  
var blogs = context.Blogs.Where(blog => blog.Contains("dotnet"))
    .OrderByDescending(blog => blog.Rating)
    .Select(blog => new
    {
        Id = blog.BlogId,
        Url = StandardizeUrl(blog.Url)
    })
    .ToList();

Wow, wow, wow, cronometrado.

Es hora de actualizar un poco su conocimiento de las técnicas LINQ.

Veamos las diferencias entre ToList AsEnumerable AsQueryable


Así que a la lista

  • .
  • .ToList() (lazy loading), .

AsEnumerable

  • (lazy loading)
  • : Func <TSource, bool>
  • ( Where/Take/Skip , , select * from Table1,
  • , N )
  • : Linq-to-SQL + Linq-to-Object.
  • IEnumerable (lazy loading).

AsQueryable

  • (lazy loading)
  • :
    AsQueryable(IEnumerable)  AsQueryable<TElement>(IEnumerable<TElement>) 

  • Expression T-SQL ( ), .
  • DbSet ( Entity Framework) AsQueryable .
  • , Take(5) «select top 5 * SQL» . , SQL , . AsQueryable() , AsEnumerable() T-SQL Linq .
  • Use AsQueryable si desea una consulta de base de datos que pueda mejorarse antes de ejecutarse en el lado del servidor.


Un ejemplo de uso de AsQueryable en el caso más simple:

public IEnumerable<EmailView> GetEmails(out int totalRecords, Guid? deviceWorkGroupID,
                DateTime? timeStart, DateTime? timeEnd, string search, int? confirmStateID, int? stateTypeID, int? limitOffset, int? limitRowCount, string orderBy, bool desc)
        {
            var r = new List<EmailView>();

            using (var db = new GJobEntities())
            {
                var query = db.Emails.AsQueryable();

                if (timeStart != null && timeEnd != null)
                {
                    query = query.Where(p => p.Created >= timeStart && p.Created <= timeEnd);
                }

                if (stateTypeID != null && stateTypeID > -1)
                {
                    query = query.Where(p => p.EmailStates.OrderByDescending(x => x.AtTime).FirstOrDefault().EmailStateTypeID == stateTypeID);
                }


                if (confirmStateID != null && confirmStateID > -1)
                {
                    var boolValue = confirmStateID == 1 ? true : false;
                    query = query.Where(p => p.IsConfirmed == boolValue);
                }

                if (!string.IsNullOrEmpty(search))
                {
                    search = search.ToLower();
                    query = query.Where(p => (p.Subject + " " + p.CopiesEmails + " " + p.ToEmails + " " + p.FromEmail + " " + p.Body)
                                        .ToLower().Contains(search));
                }

                if (deviceWorkGroupID != Guid.Empty)
                {
                    query = query.Where(x => x.SCEmails.FirstOrDefault().SupportCall.Device.DeviceWorkGroupDevices.FirstOrDefault(p => p.DeviceWorkGroupID == deviceWorkGroupID) != null);
                }

                totalRecords = query.Count();
                query = query.OrderByDescending(p => p.Created);
                if (limitOffset.HasValue)
                {
                    query = query.Skip(limitOffset.Value).Take(limitRowCount.Value);
                }
                var items = query.ToList(); //    

                foreach (var item in items)
                {
                    var n = new EmailView
                    {
                        ID = item.ID,
                        SentTime = item.SentTime,
                        IsConfirmed = item.IsConfirmed,
                        Number = item.Number,
                        Subject = item.Subject,
                        IsDeleted = item.IsDeleted,
                        ToEmails = item.ToEmails,
                        Created = item.Created,
                        CopiesEmails = item.CopiesEmails,
                        FromEmail = item.FromEmail,
                    };

                    //     - 

                    r.Add(n);
                }
            }

            return r;
        }


La magia de la lectura simple.


Si no necesita cambiar los datos, simplemente use el método .AsNoTracking () .

Muestreo lento

var blogs = context.Blogs.ToList();

Búsqueda rápida (solo lectura)

var blogs = context.Blogs.AsNoTracking().ToList();

¿Sientes que ya te has calentado un poco?

Tipos de carga de datos relacionados


Para aquellos que han olvidado lo que es la carga perezosa .

Carga diferida ( Carga diferida) significa que los datos asociados se cargan de forma transparente desde la base de datos al acceder a la propiedad de navegación. Lee más aquí .

Y al mismo tiempo, permíteme recordarte otros tipos de datos relacionados con la carga.

Carga activa ( carga ansiosa) significa que los datos asociados se cargan desde la base de datos como parte de la solicitud inicial.

using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
            .ThenInclude(post => post.Author)
                .ThenInclude(author => author.Photo)
        .Include(blog => blog.Owner)
            .ThenInclude(owner => owner.Photo)
        .ToList();
}

¡Atención! A partir de la versión EF Core 3.0.0, cada inclusión hará que se agregue un JOIN adicional a las consultas SQL generadas por los proveedores relacionales, mientras que las versiones anteriores generaron consultas SQL adicionales. Esto puede cambiar significativamente el rendimiento de sus consultas, para bien o para mal. En particular, las consultas LINQ con un número extremadamente grande de declaraciones de inclusión se pueden dividir en varias consultas LINQ separadas.

Carga explícita ( carga explícita) significa que los datos asociados se cargaron explícitamente desde la base de datos más tarde.

using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    var goodPosts = context.Entry(blog)
        .Collection(b => b.Posts)
        .Query()
        .Where(p => p.Rating > 3)
        .ToList();
}

Jerk y avance! ¿Hacia adelante?

¿Listo para acelerar aún más?


Para acelerar drásticamente cuando se obtienen datos complejamente estructurados e incluso anormales de una base de datos relacional, hay dos formas de hacerlo: usar vistas indexadas (1) o, mejor aún, datos previamente preparados (calculados) en una forma plana simple para mostrar (2).

(1) Vista indizada en el contexto de MS SQL Server

La vista indizada tiene un índice agrupado único. Un índice agrupado único se almacena en SQL Server y se actualiza como cualquier otro índice agrupado. Una vista indizada es más importante que las vistas estándar, que incluyen el procesamiento complejo de una gran cantidad de filas, por ejemplo, agregar una gran cantidad de datos o combinar varias filas.

Si a menudo se hace referencia a dichas vistas en las consultas, podemos mejorar el rendimiento creando un índice agrupado único para la vista. Para la vista estándar, el conjunto de resultados no se almacena en la base de datos; en cambio, el conjunto de resultados se calcula para cada consulta, pero en el caso de un índice agrupado, el conjunto de resultados se almacena en la base de datos de la misma manera que una tabla con un índice agrupado. Las consultas que no utilizan específicamente la vista indizada pueden incluso beneficiarse de la existencia de un índice agrupado de la vista.

La representación de un índice tiene un cierto costo en forma de productividad. Si creamos una vista indizada, cada vez que cambiamos los datos en las tablas base, SQL Server debe admitir no solo los registros de índice en estas tablas, sino también los registros de índice en la vista. En las ediciones de SQL Server para desarrolladores y empresas, el optimizador puede usar índices de vista para optimizar consultas que no especifican una vista indizada. Sin embargo, en otras ediciones de SQL Server, la consulta debe incluir una vista indizada y proporcionar una sugerencia NOEXPAND para aprovechar el índice en la vista.

(2) Si necesita realizar una solicitud que requiere la visualización de más de tres niveles de tablas relacionadas en la cantidad de tres o más con CRUD aumentadocargar, la mejor manera sería calcular periódicamente el conjunto de resultados, guardarlo en una tabla y usarlo para mostrarlo. La tabla resultante en la que se almacenarán los datos debe tener una clave primaria e índices en los campos de búsqueda en LINQ .

¿Qué pasa con la asincronía?


¡Si! ¡Lo usamos siempre que sea posible! Aquí hay un ejemplo:

public void Do()
{
    var myTask = GetFederalDistrictsAsync ();
    foreach (var item in myTask.Result)
    {
         // 
    }
}

public async Task<List<FederalDistrict>> GetFederalDistrictsAsync()
{
    var conn = configurationRoot.GetConnectionString("EFCoreTestContext");
    optionsBuilder.UseSqlServer(conn);
    using var context = new EFCoreTestContext(optionsBuilder.Options);
    return await context.FederalDistricts.ToListAsync();
}

Y sí, ¿has olvidado algo para aumentar la productividad? Buum!

return await context.FederalDistricts.<b>AsNoTracking()</b>.ToListAsync();


Nota: el método Do () se agregó solo con fines de demostración, a fin de indicar la operatividad del método GetFederalDistrictsAsync () . Como mis colegas notaron correctamente, se necesita otro ejemplo de asincronía pura.

Y permítanme darlo basado en el concepto de un componente de vista en ASP .NET Core :

//  
public class PopularPosts : ViewComponent
    {
        private readonly IStatsRepository _statsRepository;

        public PopularPosts(IStatsRepository statsRepository)
        {
            _statsRepository = statsRepository;
        }

        public async Task<IViewComponentResult> InvokeAsync()
        {
           //         -
            var federalDistricts = await _statsRepository.GetFederalDistrictsAsync(); 
            var model = new TablePageModel()
            {
                FederalDistricts = federalDistricts,
            };

            return View(model);
        }
    }
    // 
    
    /// <summary>
    ///  -   .... -
    /// </summary>
    public interface IStatsRepository
    {
        /// <summary>
        ///        
        /// </summary>
        /// <returns></returns>
        IEnumerable<FederalDistrict> FederalDistricts();

        /// <summary>
        ///        
	/// !!!
        /// </summary>
        /// <returns></returns>
        Task<List<FederalDistrict>> GetFederalDistrictsAsync();
    }	
	
    /// <summary>
    /// -   .... -
    /// </summary>
    public class StatsRepository : IStatsRepository
    {
        private readonly DbContextOptionsBuilder<EFCoreTestContext>
            optionsBuilder = new DbContextOptionsBuilder<EFCoreTestContext>();
        private readonly IConfigurationRoot configurationRoot;

        public StatsRepository()
        {
            IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
                    .SetBasePath(Environment.CurrentDirectory)
                    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
            configurationRoot = configurationBuilder.Build();
        }

        public async Task<List<FederalDistrict>> GetFederalDistrictsAsync()
        {
            var conn = configurationRoot.GetConnectionString("EFCoreTestContext");
            optionsBuilder.UseSqlServer(conn);
            using var context = new EFCoreTestContext(optionsBuilder.Options);
            return await context.FederalDistricts.Include(x => x.FederalSubjects).ToListAsync();
        }

        public IEnumerable<FederalDistrict> FederalDistricts()
        {
            var conn = configurationRoot.GetConnectionString("EFCoreTestContext");
            optionsBuilder.UseSqlServer(conn);

            using var ctx = new EFCoreTestContext(optionsBuilder.Options);
            return ctx.FederalDistricts.Include(x => x.FederalSubjects).ToList();
        }
    }

   //         Home\Index 
    <div id="tableContainer">
            @await Component.InvokeAsync("PopularPosts")
     </div>
  //   HTML      Shared\Components\PopularPosts\Default.cshtml



Permítame recordarle cuándo se ejecutan las consultas en Entity Framework Core.

Al llamar a las declaraciones de LINQ, simplemente crea una vista de consulta en la memoria. La solicitud se envía a la base de datos solo después de procesar los resultados.

Las siguientes son las operaciones más comunes que resultan en el envío de una solicitud a la base de datos.

  • Iterar sobre los resultados en un bucle for.
  • Usar un operador, como ToList, ToArray, Single, Count.
  • Enlace de datos de los resultados de la consulta a la interfaz de usuario.

¿Cómo organizar el código de EF Core en términos de arquitectura de la aplicación?


(1) Desde el punto de vista de la arquitectura de la aplicación, debe asegurarse de que el código de acceso a su base de datos esté aislado / separado en un lugar claramente definido (de forma aislada). Esto le permite encontrar el código de la base de datos que afecta el rendimiento.

(2) No mezcle el código de acceso para su base de datos con otras partes de la aplicación, como la interfaz de usuario o la API. Por lo tanto, el código de acceso a la base de datos se puede cambiar sin preocuparse por otros problemas no relacionados con la base de datos.

¿Cómo guardar datos correctamente y rápidamente usando SaveChanges ?


Si los registros insertados son iguales, tiene sentido usar una operación de guardar para todos los registros.

Incorrecto

using(var db = new NorthwindEntities())
{
var transaction = db.Database.BeginTransaction();

try
{
    //   1
    var  obj1 = new Customer();
    obj1.CustomerID = "ABCDE";
    obj1.CompanyName = "Company 1";
    obj1.Country = "USA";
    db.Customers.Add(obj1);

  //          db.SaveChanges();

    //   2
    var  obj2 = new Customer();
    obj2.CustomerID = "PQRST";
    obj2.CompanyName = "Company 2";    
    obj2.Country = "USA";
    db.Customers.Add(obj2);

    //   
    db.SaveChanges();

    transaction.Commit();
}
catch
{
    transaction.Rollback();
}
}

Correctamente

using(var db = new NorthwindEntities())
{
var transaction = db.Database.BeginTransaction();

try
{
   //  1
    var  obj1 = new Customer();
    obj1.CustomerID = "ABCDE";
    obj1.CompanyName = "Company 1";
    obj1.Country = "USA";
    db.Customers.Add(obj1); 

    //   2
    var  obj2 = new Customer();
    obj2.CustomerID = "PQRST";
    obj2.CompanyName = "Company 2";    
    obj2.Country = "USA";
    db.Customers.Add(obj2);

   //    N 
    db.SaveChanges();

    transaction.Commit();
}
catch
{
    transaction.Rollback();
}
}

Siempre hay excepciones a la regla. Si el contexto de la transacción es complejo, es decir, consta de varias operaciones independientes, puede guardar después de cada operación. Y es aún más correcto usar el almacenamiento asíncrono en una transacción.

//    
public async Task<IActionResult> AddDepositToHousehold(int householdId, DepositRequestModel model)
{
    using (var transaction = await Context.Database.BeginTransactionAsync(IsolationLevel.Snapshot))
    {
        try
        {
            //     
            var deposit = this.Mapper.Map<Deposit>(model);
            await this.Context.Deposits.AddAsync(deposit);

            await this.Context.SaveChangesAsync();

            //    
               var debtsToPay = await this.Context.Debts.Where(d => d.HouseholdId == householdId && !d.IsPaid).OrderBy(d => d.DateMade).ToListAsync();

            debtsToPay.ForEach(d => d.IsPaid = true);

            await this.Context.SaveChangesAsync();

            //   
            var household = this.Context.Households.FirstOrDefaultAsync(h => h.Id == householdId);

            household.Balance += model.DepositAmount;

            await this.Context.SaveChangesAsync();

            transaction.Commit();
            return this.Ok();
        }
        catch
        {
            transaction.Rollback();
            return this.BadRequest();
        }
    }
}

Disparadores, campos calculados, funciones personalizadas y EF Core


Para reducir la carga en las aplicaciones que contienen EF Core, tiene sentido usar campos calculados simples y disparadores de bases de datos, pero es mejor no involucrarse, ya que la aplicación puede ser muy confusa. ¡Pero las funciones definidas por el usuario pueden ser muy útiles, especialmente durante las operaciones de recuperación!

Concurrencia en EF Core


Si desea paralelizar todo para acelerar, luego interrumpa: EF Core no admite la ejecución de varias operaciones paralelas en una instancia del contexto. Espere a que se complete una operación antes de comenzar la siguiente. Para hacer esto, generalmente necesita especificar la palabra clave de espera en cada operación asincrónica.

EF Core utiliza consultas asincrónicas para evitar bloquear el flujo al ejecutar una consulta en la base de datos. Las solicitudes asincrónicas son importantes para garantizar una respuesta rápida de la interfaz de usuario en clientes gruesos. También pueden aumentar el rendimiento en una aplicación web, donde puede liberar el hilo para manejar otras solicitudes. Aquí hay un ejemplo:

public async Task<List<Blog>> GetBlogsAsync()
{
    using (var context = new BloggingContext())
    {
        return await context.Blogs.ToListAsync();
    }
}

¿Qué sabe sobre las consultas compiladas de LINQ?


Si tiene una aplicación que realiza consultas estructuralmente similares repetidamente en Entity Framework, a menudo puede mejorar el rendimiento compilando la consulta una vez y ejecutándola varias veces con diferentes parámetros. Por ejemplo, una aplicación puede necesitar obtener todos los clientes en una ciudad específica; el usuario indica la ciudad en tiempo de ejecución en el formulario. LINQ to Entities admite el uso de consultas compiladas para este propósito.

A partir de .NET Framework 4.5, las consultas LINQ se almacenan en caché automáticamente. Sin embargo, aún puede usar consultas LINQ compiladas para reducir este costo en ejecuciones posteriores, y las consultas compiladas pueden ser más eficientes que las consultas LINQ que se almacenan en caché automáticamente. Tenga en cuenta que las consultas LINQ to Entities que aplican el operador Enumerable.Contains a las colecciones en memoria no se almacenan en caché automáticamente. Además, no se permite la parametrización de colecciones en memoria en consultas LINQ compiladas.

Muchos ejemplos se pueden encontrar aquí .

¡No hagas contextos grandes de DbContext!


En general, conozco a muchos de ustedes, si no a todos, de perezosos f_u__c_k__e_r__s y de toda la base de datos que coloca en un contexto, especialmente esto es típico para el enfoque de Base de datos primero. ¡Y en vano lo haces! El siguiente es un ejemplo de cómo se puede dividir el contexto. Por supuesto, las tablas de conexión entre contextos deberán duplicarse, esto es un signo menos. De una forma u otra, si tiene más de 50 tablas en el contexto, es mejor pensar en dividirlo.

Uso de agrupación de contexto (agrupación DdContext)


El significado del grupo DbContext es permitir la reutilización de las instancias DbContext del grupo, lo que en algunos casos puede conducir a un mejor rendimiento que crear una nueva instancia cada vez. Esta es también la razón principal para crear un grupo de conexiones en ADO.NET, aunque las ganancias de rendimiento para las conexiones serán más significativas ya que las conexiones son generalmente un recurso más difícil.

using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Demos
{
    public class Blog
    {
        public int BlogId { get; set; }
        public string Name { get; set; }
        public string Url { get; set; }
    }

    public class BloggingContext : DbContext
    {
        public static long InstanceCount;

        public BloggingContext(DbContextOptions options)
            : base(options)
            => Interlocked.Increment(ref InstanceCount);

        public DbSet<Blog> Blogs { get; set; }
    }

    public class BlogController
    {
        private readonly BloggingContext _context;

        public BlogController(BloggingContext context) => _context = context;

        public async Task ActionAsync() => await _context.Blogs.FirstAsync();
    }

    public class Startup
    {
        private const string ConnectionString
            = @"Server=(localdb)\mssqllocaldb;Database=Demo.ContextPooling;Integrated Security=True;ConnectRetryCount=0";

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<BloggingContext>(c => c.UseSqlServer(ConnectionString));
        }
    }

    public class Program
    {
        private const int Threads = 32;
        private const int Seconds = 10;

        private static long _requestsProcessed;

        private static async Task Main()
        {
            var serviceCollection = new ServiceCollection();
            new Startup().ConfigureServices(serviceCollection);
            var serviceProvider = serviceCollection.BuildServiceProvider();

            SetupDatabase(serviceProvider);

            var stopwatch = new Stopwatch();

            MonitorResults(TimeSpan.FromSeconds(Seconds), stopwatch);

            await Task.WhenAll(
                Enumerable
                    .Range(0, Threads)
                    .Select(_ => SimulateRequestsAsync(serviceProvider, stopwatch)));
        }

        private static void SetupDatabase(IServiceProvider serviceProvider)
        {
            using (var serviceScope = serviceProvider.CreateScope())
            {
                var context = serviceScope.ServiceProvider.GetService<BloggingContext>();

                if (context.Database.EnsureCreated())
                {
                    context.Blogs.Add(new Blog { Name = "The Dog Blog", Url = "http://sample.com/dogs" });
                    context.Blogs.Add(new Blog { Name = "The Cat Blog", Url = "http://sample.com/cats" });
                    context.SaveChanges();
                }
            }
        }
        private static async Task SimulateRequestsAsync(IServiceProvider serviceProvider, Stopwatch stopwatch)
        {
            while (stopwatch.IsRunning)
            {
                using (var serviceScope = serviceProvider.CreateScope())
                {
                    await new BlogController(serviceScope.ServiceProvider.GetService<BloggingContext>()).ActionAsync();
                }

                Interlocked.Increment(ref _requestsProcessed);
            }
        }

        private static async void MonitorResults(TimeSpan duration, Stopwatch stopwatch)
        {
            var lastInstanceCount = 0L;
            var lastRequestCount = 0L;
            var lastElapsed = TimeSpan.Zero;

            stopwatch.Start();

            while (stopwatch.Elapsed < duration)
            {
                await Task.Delay(TimeSpan.FromSeconds(1));

                var instanceCount = BloggingContext.InstanceCount;
                var requestCount = _requestsProcessed;
                var elapsed = stopwatch.Elapsed;
                var currentElapsed = elapsed - lastElapsed;
                var currentRequests = requestCount - lastRequestCount;

                Console.WriteLine(
                    $"[{DateTime.Now:HH:mm:ss.fff}] "
                    + $"Context creations/second: {instanceCount - lastInstanceCount} | "
                    + $"Requests/second: {Math.Round(currentRequests / currentElapsed.TotalSeconds)}");

                lastInstanceCount = instanceCount;
                lastRequestCount = requestCount;
                lastElapsed = elapsed;
            }

            Console.WriteLine();
            Console.WriteLine($"Total context creations: {BloggingContext.InstanceCount}");
            Console.WriteLine(
                $"Requests per second:     {Math.Round(_requestsProcessed / stopwatch.Elapsed.TotalSeconds)}");

            stopwatch.Stop();
        }

¿Cómo evitar errores innecesarios con CRUD en EF Core?


Nunca inserte cálculos en el mismo código. Separe siempre la formación / preparación de un objeto y su inserción / actualización. Simplemente extiéndalo por función: verificando los datos ingresados ​​por el usuario, calculando los datos preliminares necesarios, mapeando o creando un objeto, y la operación CRUD real.

¿Qué hacer cuando las cosas están realmente mal con el rendimiento de la aplicación?


La cerveza definitivamente no ayudará aquí. Pero lo que ayudará es la separación de lectura y escritura en la arquitectura de la aplicación, seguida de la asignación de estas operaciones en sockets. Piense en usar el patrón de Segregación de responsabilidad de comando y consulta (CQRS) , y también intente dividir las tablas en insertar y leer entre dos bases de datos.

¡Acelere las aplicaciones para usted, amigos y colegas!

Source: https://habr.com/ru/post/undefined/


All Articles