Sunday, August 30, 2020

Tips and tricks for ASP.NET Core applications

 

This is a small collection of some tips and tricks which I keep repeating myself in every ASP.NET Core application. There's nothing ground breaking in this list, but some general advice and minor tricks which I have picked up over the course of several real world applications.

Logging

Let's begin with logging. There are many logging frameworks available for .NET Core, but my absolute favourite is Serilog which offers a very nice structured logging interface for a vast number of available storage providers (sinks).

Tip 1: Configure logging before anything else

The logger should be the very first thing configured in an ASP.NET Core application. Everything else should be wrapped in a try-catch block:

public class Program
{
    public static int Main(string[] args) => StartWebServer(args);

    public static int StartWebServer(string[] args)
    {
        Log.Logger =
            new LoggerConfiguration()
                .MinimumLevel.Warning()
                .Enrich.WithProperty("Application", "MyApplicationName")
                .WriteTo.Console()
                .CreateLogger();

        try
        {
            WebHost.CreateDefaultBuilder(args)
                .UseSerilog()
                .UseKestrel(k => k.AddServerHeader = false)
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseStartup<Startup>()
                .Build()
                .Run();

            return 0;
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Host terminated unexpectedly.");
            return -1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
}

Tip 2: Flush the logger before the application terminates

Make sure to put Log.CloseAndFlush(); into the finally block of your try-catch block so that no log data is getting lost when the application terminates before all logs have been written to the log stream.

Tip 3: Enrich your log entries

Configure your logger to automatically decorate every log entry with an Application property which contains a unique identifier for your application (typically a human readable name which identifies your app):

.Enrich.WithProperty("Application", "MyApplicationName")

This is extremely useful if you write logs from more than one application into a single log stream (e.g. a single Elasticsearch database). Personally I prefer to write logs from multiple (smaller) services of a coherent system into a single logging database and filter logs by properties.

Appending an additional Application property to all your application logs has the advantage that one can easily filter and view the overall health of a single application as well as getting a holistic view of the entire system.

Other really useful information which could be appended to your log entries is the application version and the environment name:

Log.Logger =
    new LoggerConfiguration()
        .MinimumLevel.Warning()
        .Enrich.WithProperty("Application", "MyApplicationName")
        .Enrich.WithProperty("ApplicationVersion", "<version number>")
        .Enrich.WithProperty("EnvironmentName", "Staging")
        .WriteTo.Console()
        .CreateLogger();

This will allow one to better visualise if issues had been resolved (or appeared) after a certain version has been deployed and it will also make it very easy to filter out any logs which might have accidentally been written from a different environment (e.g. a developer was debugging locally with the production connection string in their settings).

Startup Configuration

In ASP.NET Core there are two main places where features and functionality get configured. First there is the Configure method which can be used to plug middleware into the ASP.NET Core pipeline and secondly there is the ConfigureServices method to register dependencies.

For example adding Swagger to ASP.NET Core would look a bit like this:

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

    services.AddSwaggerGen(
        c =>
        {
            var name = "<my app name>"
            var version = "v1";

            c.SwaggerDoc(
                version,
                new Info { Version = version, Title = name });

            c.DescribeAllEnumsAsStrings();

            var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
            var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
            c.IncludeXmlComments(xmlPath);
        });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .UseSwagger()
        .UseSwaggerUI(
            c =>
            {
                var name = "<my app name>"
                var version = "v1";
                c.RoutePrefix = "";
                c.SwaggerEndpoint(
                    "/swagger/{version}/swagger.json", name);
            }
        )
        .UseMvc();
}

Middleware and dependencies are obviously two different things and therefore their configuration is split into two different methods, but from a developer's point of view it is very annoying that most features are configured across more than just one place.

Tip 4: Create 'Config' classes

One nice way to combat this is by creating a Config folder in the root of your ASP.NET Core application and create <FeatureName>Config classes for each feature/functionality which needs to be registered in Startup:

public static class SwaggerConfig
{
    private static string Name => "My Cool API";
    private static string Version => "v1";
    private static string Endpoint => $"/swagger/{Version}/swagger.json";
    private static string UIEndpoint => "";

    public static void SwaggerUIConfig(SwaggerUIOptions config)
    {
        config.RoutePrefix = UIEndpoint;
        config.SwaggerEndpoint(Endpoint, Name);
    }

    public static void SwaggerGenConfig(SwaggerGenOptions config)
    {
        config.SwaggerDoc(
            Version,
            new Info { Version = Version, Title = Name });

        config.DescribeAllEnumsAsStrings();

        var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
        var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
        config.IncludeXmlComments(xmlPath);
    }
}

By doing this one can move all related configuration of a feature into a single place and also nicely distinguish between the individual configuration steps (e.g. SwaggerUIConfig vs SwaggerGenConfig).

Afterwards one can tidy up the Startup class by invoking the respective class methods:

public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvc(MvcConfig.AddFilters)
        .AddJsonOptions(MvcConfig.JsonOptions);

    services.AddSwaggerGen(SwaggerConfig.SwaggerGenConfig);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .UseSwagger()
        .UseSwaggerUI(SwaggerConfig.SwaggerUIConfig)
        .UseMvc();
}

Tip 5: Extension methods for conditional configurations

Another common use case is to configure different features based on the current environment or other conditional cases:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
}

A neat trick which I like to apply here is to implement an extension method for conditional configurations:

public static class ApplicationBuilderExtensions
{
    public static IApplicationBuilder When(
        this IApplicationBuilder builder,
        bool predicate,
        Func<IApplicationBuilder> compose) => predicate ? compose() : builder;
}

The When extension method will invoke a compose function only if a given predicate is true.

Now with the When method someone can set up conditional middleware in a much nicer and fluent way:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .When(env.IsDevelopment(), app.UseDeveloperExceptionPage)
        .When(!env.IsDevelopment(), app.UseHsts)
        .UseSwagger()
        .UseSwaggerUI(SwaggerConfig.SwaggerUIConfig)
        .UseMvc();
}

Exit scenarios

Tip 6: Don't forget to return a default 404 response

Don't forget to register a middleware which will return a 404 Not Found HTTP response if no other middleware was able to deal with an incoming request:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app .When(env.IsDevelopment(), app.UseDeveloperExceptionPage)
        .When(!env.IsDevelopment(), app.UseHsts)
        .UseSwagger()
        .UseSwaggerUI(SwaggerConfig.SwaggerUIConfig)
        .UseMvc()
        .Run(NotFoundHandler);
}

private readonly RequestDelegate NotFoundHandler =
    async ctx =>
    {
        ctx.Response.StatusCode = 404;
        await ctx.Response.WriteAsync("Page not found.");
    };

If you don't do this then a request which couldn't be matched by any middleware will be left unhandled (unless you have another web server sitting behind Kestrel).

Tip 7: Return non zero exit code on failure

Return a non zero exit code when the application terminates with an error. This will allow parent processes to pick up the fact that the application terminated unexpectedly and give them a chance to handle such a situation more gracefully (e.g. when your ASP.NET Core application is run from a Kubernetes cluster):

try
{
    // Start WebHost

    return 0;
}
catch (Exception ex)
{
    Log.Fatal(ex, "Host terminated unexpectedly.");
    return -1;
}
finally
{
    Log.CloseAndFlush();
}

Error Handling

Every ASP.NET Core application is likely going to have to deal with at least three types of errors:

  • Server errors
  • Client errors
  • Business logic errors

Server errors are unexpected exceptions which get thrown by an application. Normally these exceptions bubble up to a global error handler which will log the exception and return a 500 Internal Server Error response to the client.

Client errors are mistakes which a client can make when sending a request to the server. These normally include things like missing or wrong authentication data, badly formatted request bodies, calling endpoints which do not exist or perhaps sending data in an unsupported format. Most of these errors will get picked up by a built-in ASP.NET Core feature which will return a corresponding 4xx HTTP error back to the client.

Business logic errors are application specific errors which are not handled by ASP.NET Core by default because they are very unique to each individual application. For example an invoicing application might want to throw an exception when a customer tries to raise an invoice with an unsupported currency whereas an online gaming application might want to throw an error when a user ran out of credits.

These errors are often raised from lower level domain code and might want to return a specific 4xx or 5xx HTTP response back to the client.

Tip 8: Create a base exception type for domain errors

Create a base exception class for business or domain errors and additional exception classes which derive from the base class for all possible error cases:

public enum DomainErrorCode
{
    InsufficientCredits = 1000
}

public class DomainException : Exception
{
    public readonly DomainErrorCode ErrorCode;

    public DomainException(DomainErrorCode code, string message) : base(message)
    {
        ErrorCode = code;
    }
}

public class InsufficientCreditsException : DomainException
{
    public InsufficientCreditsException()
        : base(DomainErrorCode.InsufficientCredits,
                "User ran out of free credit. Please upgrade your plan to continue using our service.")
    { }
}

Include a unique DomainErrorCode for each custom exception type which later can be used to identify the specific error case from higher level code.

Afterwards one can use the newly created exception classes to throw more meaningful errors from inside the domain layer:

throw new InsufficientCreditsException();

This has now the benefit that the ASP.NET Core application can look for domain exceptions from a central point (e.g. custom error middleware) and handle them accordingly:

public class DomainErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;

    public DomainErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next ?? throw new ArgumentNullException(nameof(next));
    }

    public async Task InvokeAsync(HttpContext ctx)
    {
        try
        {
            await _next(ctx);
        }
        catch(DomainException ex)
        {
            ctx.Response.StatusCode = 422;
            await ctx.Response.WriteAsync($"{ex.ErrorCode}: {ex.Message}");
        }
    }
}

Because every domain exception includes a unique DomainErrorCode the generic error handler can even implement a slightly different response based on the given domain error.

This architecture has a few benefits:

  • The domain layer can throw meaningful exceptions
  • The domain layer works nicely with the higher level web layer without tight coupling
  • Domain exceptions are clearly distinguishable from other errors
  • Domain exceptions are self documenting
  • The web layer can handle all domain errors in a unified way without having to replicate the same try-catch block across multiple controllers
  • The additional error code in the response can be parsed and understood by third party clients
  • The custom exception types can be easily documented through Swagger

Tip 9: Expose an endpoint which returns all error codes

When you followed tip 8 and implemented a custom exception type with a unique error code for each error case then it can be extremely handy to expose all possible error codes through a single API endpoint. This will allow third party clients to quickly retrieve a list of the latest possible error codes and their meaning:

[HttpGet("/error-codes")]
public ActionResult<IDictionary<int, string>> ErrorCodes()
{
    var values = Enum
        .GetValues(typeof(DomainErrorCode))
        .Cast<DomainErrorCode>();

    var result = new Dictionary<int, string>();

    foreach(var v in values)
        result.Add((int)v, v.ToString());

    return result;
}

Other Tips & Tricks

Tip 10: Expose a version endpoint

Another really useful thing to have in an API (or website) is a version endpoint. Often it can be extremely helpful to customer support staff, QA or other members of a team to quickly be able to establish what version of an application is being deployed to an environment.

This version is different than the customer facing API version which often only includes the major version number (e.g. https://my-api.com/v3/some/resource).

Exposing an endpoint which displays the current application version and the build date and time is a nice way of quickly making this information accessible to relevant people:

[HttpGet("/info")]
public ActionResult<string> Info()
{
    var assembly = typeof(Startup).Assembly;

    var creationDate = File.GetCreationTime(assembly.Location);
    var version = FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion;

    return Ok($"Version: {version}, Last Updated: {creationDate}");
}

Tip 11: Remove the 'Server' HTTP header

Whilst one is at configuring their ASP.NET Core application they might as well remove the Server HTTP header from every HTTP response by deactivating that setting in Kestrel:

.UseKestrel(k => k.AddServerHeader = false)

Tip 12: Working with Null Collections

My last tip on this list is not specific to ASP.NET Core but all of .NET Core development where a collection or IEnumerable type is being used.

How often do .NET developers write something like this:

var someCollection = GetSomeCollectionFromSomewhere();

if (someCollection != null && someCollection.Count > 0)
{
    foreach(var item in someCollection)
    {
        // Do stuff
    }
}

Adding a one line extension method can massively simplify the above code across an entire applicatoin:

public static class EnumerableExtensions
{
    public static IEnumerable<T> OrEmptyIfNull<T>(this IEnumerable<T> source) =>
        source ?? Enumerable.Empty<T>();
}

Now the above if statement can be reduced to a single loop like this:

var someCollection = GetSomeCollectionFromSomewhere();

foreach(var item in someCollection.OrEmptyIfNull())
{
    // Do stuff
}

Or converting the IEnumerbale to an IList and use the ForEach LINQ extension method to turn this into a one liner:

someCollection.OrEmptyIfNull().ToList().ForEach(i => i.DoSomething());

What tips and tricks do you have?

So this is it, this was my brief post on some tips and tricks which I like to apply in my personal ASP.NET Core development. I hope this was at least somewhat useful to someone?! Let me know what you think and please feel free to share your own tips and tricks which make your ASP.NET Core development life easier in the comments below!

Tutorial: Learn about advanced scenarios - ASP.NET MVC with EF Core

 

In the previous tutorial, you implemented table-per-hierarchy inheritance. This tutorial introduces several topics that are useful to be aware of when you go beyond the basics of developing ASP.NET Core web applications that use Entity Framework Core.

In this tutorial, you:

  • Perform raw SQL queries
  • Call a query to return entities
  • Call a query to return other types
  • Call an update query
  • Examine SQL queries
  • Create an abstraction layer
  • Learn about Automatic change detection
  • Learn about EF Core source code and development plans
  • Learn how to use dynamic LINQ to simplify code

Prerequisites

Perform raw SQL queries

One of the advantages of using the Entity Framework is that it avoids tying your code too closely to a particular method of storing data. It does this by generating SQL queries and commands for you, which also frees you from having to write them yourself. But there are exceptional scenarios when you need to run specific SQL queries that you have manually created. For these scenarios, the Entity Framework Code First API includes methods that enable you to pass SQL commands directly to the database. You have the following options in EF Core 1.0:

  • Use the DbSet.FromSql method for queries that return entity types. The returned objects must be of the type expected by the DbSet object, and they're automatically tracked by the database context unless you turn tracking off.

  • Use the Database.ExecuteSqlCommand for non-query commands.

If you need to run a query that returns types that aren't entities, you can use ADO.NET with the database connection provided by EF. The returned data isn't tracked by the database context, even if you use this method to retrieve entity types.

As is always true when you execute SQL commands in a web application, you must take precautions to protect your site against SQL injection attacks. One way to do that is to use parameterized queries to make sure that strings submitted by a web page can't be interpreted as SQL commands. In this tutorial you'll use parameterized queries when integrating user input into a query.

Call a query to return entities

The DbSet<TEntity> class provides a method that you can use to execute a query that returns an entity of type TEntity. To see how this works you'll change the code in the Details method of the Department controller.

In DepartmentsController.cs, in the Details method, replace the code that retrieves a department with a FromSql method call, as shown in the following highlighted code:

C#
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
    var department = await _context.Departments
        .FromSql(query, id)
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync();

    if (department == null)
    {
        return NotFound();
    }

    return View(department);
}

To verify that the new code works correctly, select the Departments tab and then Details for one of the departments.

Department Details

Call a query to return other types

Earlier you created a student statistics grid for the About page that showed the number of students for each enrollment date. You got the data from the Students entity set (_context.Students) and used LINQ to project the results into a list of EnrollmentDateGroup view model objects. Suppose you want to write the SQL itself rather than using LINQ. To do that you need to run a SQL query that returns something other than entity objects. In EF Core 1.0, one way to do that is write ADO.NET code and get the database connection from EF.

In HomeController.cs, replace the About method with the following code:

C#
public async Task<ActionResult> About()
{
    List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
    var conn = _context.Database.GetDbConnection();
    try
    {
        await conn.OpenAsync();
        using (var command = conn.CreateCommand())
        {
            string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
                + "FROM Person "
                + "WHERE Discriminator = 'Student' "
                + "GROUP BY EnrollmentDate";
            command.CommandText = query;
            DbDataReader reader = await command.ExecuteReaderAsync();

            if (reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
                    groups.Add(row);
                }
            }
            reader.Dispose();
        }
    }
    finally
    {
        conn.Close();
    }
    return View(groups);
}

Add a using statement:

C#
using System.Data.Common;

Run the app and go to the About page. It displays the same data it did before.

About page

Call an update query

Suppose Contoso University administrators want to perform global changes in the database, such as changing the number of credits for every course. If the university has a large number of courses, it would be inefficient to retrieve them all as entities and change them individually. In this section you'll implement a web page that enables the user to specify a factor by which to change the number of credits for all courses, and you'll make the change by executing a SQL UPDATE statement. The web page will look like the following illustration:

Update Course Credits page

In CoursesController.cs, add UpdateCourseCredits methods for HttpGet and HttpPost:

C#
public IActionResult UpdateCourseCredits()
{
    return View();
}
C#
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
    if (multiplier != null)
    {
        ViewData["RowsAffected"] = 
            await _context.Database.ExecuteSqlCommandAsync(
                "UPDATE Course SET Credits = Credits * {0}",
                parameters: multiplier);
    }
    return View();
}

When the controller processes an HttpGet request, nothing is returned in ViewData["RowsAffected"], and the view displays an empty text box and a submit button, as shown in the preceding illustration.

When the Update button is clicked, the HttpPost method is called, and multiplier has the value entered in the text box. The code then executes the SQL that updates courses and returns the number of affected rows to the view in ViewData. When the view gets a RowsAffected value, it displays the number of rows updated.

In Solution Explorer, right-click the Views/Courses folder, and then click Add > New Item.

In the Add New Item dialog, click ASP.NET Core under Installed in the left pane, click Razor View, and name the new view UpdateCourseCredits.cshtml.

In Views/Courses/UpdateCourseCredits.cshtml, replace the template code with the following code:

CSHTML
@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)
{
    <form asp-action="UpdateCourseCredits">
        <div class="form-actions no-color">
            <p>
                Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
            </p>
            <p>
                <input type="submit" value="Update" class="btn btn-default" />
            </p>
        </div>
    </form>
}
@if (ViewData["RowsAffected"] != null)
{
    <p>
        Number of rows updated: @ViewData["RowsAffected"]
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

Run the UpdateCourseCredits method by selecting the Courses tab, then adding "/UpdateCourseCredits" to the end of the URL in the browser's address bar (for example: http://localhost:5813/Courses/UpdateCourseCredits). Enter a number in the text box:

Update Course Credits page

Click Update. You see the number of rows affected:

Update Course Credits page rows affected

Click Back to List to see the list of courses with the revised number of credits.

Note that production code would ensure that updates always result in valid data. The simplified code shown here could multiply the number of credits enough to result in numbers greater than 5. (The Credits property has a [Range(0, 5)] attribute.) The update query would work but the invalid data could cause unexpected results in other parts of the system that assume the number of credits is 5 or less.

For more information about raw SQL queries, see Raw SQL Queries.

Examine SQL queries

Sometimes it's helpful to be able to see the actual SQL queries that are sent to the database. Built-in logging functionality for ASP.NET Core is automatically used by EF Core to write logs that contain the SQL for queries and updates. In this section you'll see some examples of SQL logging.

Open StudentsController.cs and in the Details method set a breakpoint on the if (student == null) statement.

Run the app in debug mode, and go to the Details page for a student.

Go to the Output window showing debug output, and you see the query:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
    SELECT TOP(1) [s0].[ID]
    FROM [Person] AS [s0]
    WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
    ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

You'll notice something here that might surprise you: the SQL selects up to 2 rows (TOP(2)) from the Person table. The SingleOrDefaultAsync method doesn't resolve to 1 row on the server. Here's why:

  • If the query would return multiple rows, the method returns null.
  • To determine whether the query would return multiple rows, EF has to check if it returns at least 2.

Note that you don't have to use debug mode and stop at a breakpoint to get logging output in the Output window. It's just a convenient way to stop the logging at the point you want to look at the output. If you don't do that, logging continues and you have to scroll back to find the parts you're interested in.

Create an abstraction layer

Many developers write code to implement the repository and unit of work patterns as a wrapper around code that works with the Entity Framework. These patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application. Implementing these patterns can help insulate your application from changes in the data store and can facilitate automated unit testing or test-driven development (TDD). However, writing additional code to implement these patterns isn't always the best choice for applications that use EF, for several reasons:

  • The EF context class itself insulates your code from data-store-specific code.

  • The EF context class can act as a unit-of-work class for database updates that you do using EF.

  • EF includes features for implementing TDD without writing repository code.

For information about how to implement the repository and unit of work patterns, see the Entity Framework 5 version of this tutorial series.

Entity Framework Core implements an in-memory database provider that can be used for testing. For more information, see Test with InMemory.

Automatic change detection

The Entity Framework determines how an entity has changed (and therefore which updates need to be sent to the database) by comparing the current values of an entity with the original values. The original values are stored when the entity is queried or attached. Some of the methods that cause automatic change detection are the following:

  • DbContext.SaveChanges

  • DbContext.Entry

  • ChangeTracker.Entries

If you're tracking a large number of entities and you call one of these methods many times in a loop, you might get significant performance improvements by temporarily turning off automatic change detection using the ChangeTracker.AutoDetectChangesEnabled property. For example:

C#
_context.ChangeTracker.AutoDetectChangesEnabled = false;

EF Core source code and development plans

The Entity Framework Core source is at https://github.com/dotnet/efcore. The EF Core repository contains nightly builds, issue tracking, feature specs, design meeting notes, and the roadmap for future development. You can file or find bugs, and contribute.

Although the source code is open, Entity Framework Core is fully supported as a Microsoft product. The Microsoft Entity Framework team keeps control over which contributions are accepted and tests all code changes to ensure the quality of each release.

Reverse engineer from existing database

To reverse engineer a data model including entity classes from an existing database, use the scaffold-dbcontext command. See the getting-started tutorial.

Use dynamic LINQ to simplify code

The third tutorial in this series shows how to write LINQ code by hard-coding column names in a switch statement. With two columns to choose from, this works fine, but if you have many columns the code could get verbose. To solve that problem, you can use the EF.Property method to specify the name of the property as a string. To try out this approach, replace the Index method in the StudentsController with the following code.

C#
 public async Task<IActionResult> Index(
     string sortOrder,
     string currentFilter,
     string searchString,
     int? pageNumber)
 {
     ViewData["CurrentSort"] = sortOrder;
     ViewData["NameSortParm"] = 
         String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
     ViewData["DateSortParm"] = 
         sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

     if (searchString != null)
     {
         pageNumber = 1;
     }
     else
     {
         searchString = currentFilter;
     }

     ViewData["CurrentFilter"] = searchString;

     var students = from s in _context.Students
                    select s;
     
     if (!String.IsNullOrEmpty(searchString))
     {
         students = students.Where(s => s.LastName.Contains(searchString)
                                || s.FirstMidName.Contains(searchString));
     }

     if (string.IsNullOrEmpty(sortOrder))
     {
         sortOrder = "LastName";
     }

     bool descending = false;
     if (sortOrder.EndsWith("_desc"))
     {
         sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
         descending = true;
     }

     if (descending)
     {
         students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
     }

     int pageSize = 3;
     return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), 
         pageNumber ?? 1, pageSize));
 }

Acknowledgments

Tom Dykstra and Rick Anderson (twitter @RickAndMSFT) wrote this tutorial. Rowan Miller, Diego Vega, and other members of the Entity Framework team assisted with code reviews and helped debug issues that arose while we were writing code for the tutorials. John Parente and Paul Goldman worked on updating the tutorial for ASP.NET Core 2.2.

Troubleshoot common errors

ContosoUniversity.dll used by another process

Error message:

Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process.

Solution:

Stop the site in IIS Express. Go to the Windows System Tray, find IIS Express and right-click its icon, select the Contoso University site, and then click Stop Site.

Migration scaffolded with no code in Up and Down methods

Possible cause:

The EF CLI commands don't automatically close and save code files. If you have unsaved changes when you run the migrations add command, EF won't find your changes.

Solution:

Run the migrations remove command, save your code changes and rerun the migrations add command.

Errors while running database update

It's possible to get other errors when making schema changes in a database that has existing data. If you get migration errors you can't resolve, you can either change the database name in the connection string or delete the database. With a new database, there's no data to migrate, and the update-database command is much more likely to complete without errors.

The simplest approach is to rename the database in appsettings.json. The next time you run database update, a new database will be created.

To delete a database in SSOX, right-click the database, click Delete, and then in the Delete Database dialog box select Close existing connections and click OK.

To delete a database by using the CLI, run the database drop CLI command:

.NET Core CLI
dotnet ef database drop

Error locating SQL Server instance

Error Message:

A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)

Solution:

Check the connection string. If you have manually deleted the database file, change the name of the database in the construction string to start over with a new database.

Get the code

Download or view the completed application.

Additional resources

For more information about EF Core, see the Entity Framework Core documentation. A book is also available: Entity Framework Core in Action.

For information on how to deploy a web app, see Host and deploy ASP.NET Core.

For information about other topics related to ASP.NET Core MVC, such as authentication and authorization, see Introduction to ASP.NET Core.

Next steps

In this tutorial, you:

  • Performed raw SQL queries
  • Called a query to return entities
  • Called a query to return other types
  • Called an update query
  • Examined SQL queries
  • Created an abstraction layer
  • Learned about Automatic change detection
  • Learned about EF Core source code and development plans
  • Learned how to use dynamic LINQ to simplify code

This completes this series of tutorials on using the Entity Framework Core in an ASP.NET Core MVC application. This series worked with a new database; an alternative is to reverse engineer a model from an existing database.

Free hosting web sites and features -2024

  Interesting  summary about hosting and their offers. I still host my web site https://talash.azurewebsites.net with zero cost on Azure as ...