1
\$\begingroup\$

I'm using C#, .NET Core and the entity framework and am currently working on optimizing various functions for better performance.

One of the main functions is getting a calculation based on two indexes; a product and machine. The records in this database table is currently at 500 rows but can go upwards to 10.000.

My code for finding a record in this table is as follows:

public Calculation GetCalculationByProduct(Machine currentMachine, Product currentProduct)
{
    IList<Calculation> calculations = _calculationRepository.Index().ToList();

    for (int i = 0; i < calculations.Count(); i++)
    {
      if (calculations[i].MachineForeignKey == currentMachine.ID && calculations[i].ProductRef.ID == currentProduct.ID)
      {
        return calculations[i];
      }
    }
        return null;
    }

The function searches for the relative calculation based on both the product and the machine; since a machine and a product can both have multiple calculations.

The actual model looks like this:

public class Calculation
{
    public int ID { get; set; }

    public int MachineForeignKey { get; set; }

    public Machine MachineRef { get; set; }

    public Product ProductRef {get; set; }

    public double Value { get; set; }
}

A product model and Machine model both use this like so:

public class Product
{
    [Key]
    public int ID { get; set; }

    [Required]
    public string ProductName { get; set; }

    public string Name { get; set; }

    public IEnumerable<Calculation> Calculations { get; set; }
}

public class Machine
{
    [Key]
    public int ID { get; set; }

    public string OrderNo { get; set; }

    public IEnumerable<Calculation> Calculations { get; set; }
}

The repository used is a basic linq implementation using generics like so:

    public IEnumerable<TEntity> Index()
    {
        return _context.Set<TEntity>();
    }

And the relations for the calculation / model have been set with a modelbuilder like this:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Machine>()
            .HasMany(m => m.Calculations)
            .WithOne(s => s.MachineRef)
            .OnDelete(DeleteBehavior.Cascade);

        modelBuilder.Entity<Product>()
            .HasMany(m => m.CalculatedValues)
            .WithOne(s => s.ProductRef)
            .OnDelete(DeleteBehavior.Cascade);

    }

I first used LINQ like so:

 _calculationRepository.Index().Where(c => c.MachineForeignKey == Current_Machine.ID && c.ProductRef.ID == Current_Product.ID).FirstOrDefault();

But that caused quite bad performance; where it was around 2000ms

Currently, it goes through 500 records in about 900ms. Using visual studio's diagnostic tools I could see that this function was slowing everything down quite a lot; especially since the function is used for nearly every calculation done.

I'm looking to optimize the code, but any other advice is welcome.

\$\endgroup\$
7
  • 1
    \$\begingroup\$ Specify the exact version of the Entity Framework. Remove the asp.net tag. \$\endgroup\$ Aug 4, 2021 at 10:03
  • 1
    \$\begingroup\$ Change IEnumerable<TEntity> Index() to IQueryable<TEntity> Index(). As a result, the linq query with FirstOrDefault should work with lightning speed. \$\endgroup\$ Aug 4, 2021 at 10:06
  • \$\begingroup\$ Tag has been removed; but changing IEnumerable to IQueryable doesn't really impact the performance as far as I can see. \$\endgroup\$ Aug 4, 2021 at 10:08
  • 1
    \$\begingroup\$ Did you try it? With IEnumerable, all data will be pumped to the client. With IQueryable, the query will be executed completely in the database, \$\endgroup\$ Aug 4, 2021 at 10:09
  • \$\begingroup\$ Is if EF6? EF Core? What exact version? \$\endgroup\$ Aug 4, 2021 at 10:13

2 Answers 2

4
\$\begingroup\$

When using EF, it would be better if you query the database then create a list of the results. in GetCalculationByProduct method, see this line :

IList<Calculation> calculations = _calculationRepository.Index().ToList();

at this line, it will populate all table records and stores it in memory, which is something you don't need to be a habit in most cases.

While this line of code :

 _calculationRepository.Index().Where(c => c.MachineForeignKey == Current_Machine.ID && c.ProductRef.ID == Current_Product.ID).FirstOrDefault();

it'll build the SQL query, then execute it, and then gets the first record.

So, what you are actually missing here is AsNoTracking. You can do it like this :

_calculationRepository.Index().AsNoTracking().FirstOrDefault(c => c.MachineForeignKey == Current_Machine.ID && c.ProductRef.ID == Current_Product.ID);

by default, EF catches any returned entity, so any changes to that entity can be saved by running SaveChanges method. If you want to return results that you don't want to do any changes to them (just read-only results) use AsNoTracking this would make the execution a bit faster as the entity results won't be cached.

\$\endgroup\$
3
  • \$\begingroup\$ Using asNoTracking does have a significant impact; but are there any side effects of using that comment? I do need to update the obtained value further down the line \$\endgroup\$ Aug 4, 2021 at 6:46
  • 1
    \$\begingroup\$ @RDAxRoadkill the only side effects is that the retrieved won't be tracked, so any changes to the retrieved entries will need to Attach it and change its state manually, and also you might to use Load method to re-load entity values from the database with the new values. \$\endgroup\$
    – iSR5
    Aug 4, 2021 at 7:09
  • \$\begingroup\$ The issue here seems to be related more to the IEnumerable return type of Index() (as opposed to IQueryable. Tracking should be negligible performance-wise for fetching one entity. \$\endgroup\$
    – Flater
    Aug 18, 2021 at 9:33
4
\$\begingroup\$

Disclaimer: my practical knowledge of EF is limited. But when you are using a database the first step is to run an execution plan against your queries. Make sure that they run fast enough and within predictable times regardless of how many records you are storing.

You may have to add indexes, for instances on fields being searched or JOIned. So I would start by reviewing the table structure and the overall data model. It may be lacking optimization. If the DB structure is the problem EF will not improve things, although there are things it can do like caching data or the execution plan itself for reuse.

Also ask yourself if EF+LINQ is really the best way to do it, or if using a stored procedure would make sense perhaps. The whole logic does not necessarily needs to be handled by the client. Stored procedures can be used with EF too.

As a rule you don't want to pull out more data to the client than strictly necessary.

Suggested reading:

\$\endgroup\$
2
  • \$\begingroup\$ Thanks for the interesting links; I'll definitely have a look at those. As for the database optimization itself I'll have to do some additional research; I do believe the ID's both have an index. \$\endgroup\$ Aug 4, 2021 at 6:45
  • \$\begingroup\$ As to the stored procedure suggestion, while there are fringe cases where it makes sense to delegate work to a sproc, fetching a row based on two columns values is not such a case. Barring those fringe cases, sprocs should generally be avoided when using EF simply because it start negating the code-driven aspect of having EF as your ORM. \$\endgroup\$
    – Flater
    Aug 18, 2021 at 9:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.