Entity Framework Core’da Global Query Filters Kullanımı

Okuma Süresi: 4 dakika

Global Query Filters, entity nesnelerinde özellikle Where sorgu operatörüyle gönderdiğimiz sorguların her sorguya otomatik olarak eklemek için kullanılır. Kullanım senaryolarına örnek vermek gerekirse soft delete veya multi tenancy sorguları verilebilir. Bu yazıda örnek olması açısından soft delete senaryosu üzerinden ilerleyeceğiz. Global Query Filter tanımları DbContext sınıfının virtual olan OnModelCreating metodunda tanımlanır.

Bu uygulamamızda yapacağımız örnekte Users tablosunda IsRemoved kolonunun değeri false olan kullanıcıların listesinin gelmesi sağlanacaktır. Global Query Filter kullanmadan önce yapacağımız sorgu aşağıki gibidir.

context.Users.Where(u => !u.IsRemoved).ToList();

Normalde bu kullanım doğrudur. Ancak kodumuzun birçok yerinde Users tablosundan veri çekerken Where ile sürekli IsRemoved kontrolünün yapılması gerekmektedir. Bu işlemde kodun bakım maliyetini artırmaktadır. Bu işlemi Global Query Filter ile otomatikleştirerek kodumuzu aşağıdaki şekilde kullanmaya başlayacağız.

context.Users.ToList();

Şimdi uygulamamızı geliştirmeye başlayabiliriz. Örnek uygulama için ASP.NET Core MVC projesi oluşturacağım. Aşağıdaki komutu terminale yazarak yeni bir proje oluşturuyorum.

dotnet new mvc -n EFCore.GlobalQueryFiltersSample

İlk önce projeme IRemovable isminde bir interface ekliyorum ve içeriğini aşağıdaki şekilde düzenliyorum.

public interface IRemovable
{
    bool IsRemoved { get; set; }
}

Projeme SampleDbContext isminde bir sınıf ekliyorum ve içeriğini aşağıdaki şekilde değiştiriyorum.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace EFCore.GlobalQueryFiltersSample
{
    [Table("Users")]
    public class User : IRemovable
    {
        [Key]
        public Guid UserId { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        public bool IsRemoved { get; set; }
    }

    public class SampleDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }

        public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<User>().HasQueryFilter(u => !u.IsRemoved);
            base.OnModelCreating(modelBuilder);
        }
    }
}

Global Query Filter tanımını SampleDbContext sınıfının 29. satırında yaptık. Global Query Filter tanımını HasQueryFilter metodu aracılığıyla yapmaktayız. Parametre olarak aldığı Expression ile sorgu tanımımızı gerçekleştirdik. Bu sorgu tanımına göre User tipindeki nesnelere ait verileri getirirken IsRemoved alanındaki değeri false olanları getirmesini sağladık.

Bu işlemleri yaptıktan sonra Entity Framework’ün ayarlarını yapmaya geldi. Ben bu yazı için InMemory database kullanacağım. Entity framework’ü InMemory database ile kullanabilmek için Microsoft.EntityFrameworkCore.InMemory paketini projeye eklememiz gerekmektedir. Projeye paketi eklemek için aşağıdaki komutu terminale yazmamız gerekmektedir.

dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 2.2.4

İlgili paketi projeye ekledikten sonra DbContext’imizi ASP.NET Core uygulamıza tanıtmak kaldı. İlgili işlemi Startup.cs dosyamızda yapacağız. Startup.cs dosyamızın içeriği aşağıdaki gibidir.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EFCore.GlobalQueryFiltersSample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext<SampleDbContext>(options => options.UseInMemoryDatabase("Sample"));
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Şimdi controller’ımıza geçip uygulamamızı test etmeye geldi. Ben HomeController üzerinde çalışacağım. İçeriği aşağıdaki gibidir.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using EFCore.GlobalQueryFiltersSample.Models;

namespace EFCore.GlobalQueryFiltersSample.Controllers
{
    public class HomeController : Controller
    {
        private readonly SampleDbContext _context;

        public HomeController(SampleDbContext context)
        {
            _context = context;

            var users = new List<User>
                {
                    new User {
                        UserId = Guid.NewGuid(),
                        UserName = "mennan",
                        Name = "Mennan",
                        IsRemoved = false
                    },
                    new User {
                        UserId = Guid.NewGuid(),
                        UserName = "anil",
                        Name = "Anıl",
                        IsRemoved = true
                    }
                };

            _context.Users.AddRange(users);
            _context.SaveChanges();
        }

        public IActionResult Index()
        {
            var users = _context.Users.ToList();

            return View(users);
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Controller’ın constructor metodunda DbContext’imizin instance’ı DI ile alıyoruz ve örnek verilerimizi veritabanına kaydediyoruz. Index action’ında ise veritabanındaki tüm kayıtların listesini alıyoruz. Şimdi Index view’ın içeriğini aşağıdaki şekilde değiştiriyoruz.

@model List<User>
@{
    ViewData["Title"] = "Home Page";
}

<ul>
    @foreach (var user in Model)
    {
        <li>
            @user.Name - @user.UserName
        </li>
    }
</ul>

Gerekli değişiklikleri yaptıktan sonra sıra uygulamamızı test etmeye geldi. Terminalden dotnet run komutunu yazarak uygulamamızı tarayıcı üzerinden açalım.

Normalde veritabanımızda 2 adet kayıt olduğu halde sadece IsRemoved değeri false olan kayıt gelmiştir.

Bazı durumlarda GlobalQueryFilter’ı pasif hale getirmemiz gerekebilir. Bunun için LINQ sorgunuza IgnoreQueryFilters metodunu eklemeniz gerekmektedir. Örnek kullanımı aşağıdaki gibidir.

context.Users.IgnoreQueryFilters().ToList();

IRemovable’dan türeyen tüm entity tipleri için tek tek Global Query Filter tanımı yapmamıza gerek yok. Yapmamız gereken reflection ile IRemovable arayüzünden türeyen tipleri bulup bunları DbContext’in OnModelCreating metodunda dinamik olarak eklemek. Bunun için SampleDbContext sınıfımızı aşağıdaki şekilde düzenliyoruz.

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace EFCore.GlobalQueryFiltersSample
{
    [Table("Users")]
    public class User : IRemovable
    {
        [Key]
        public Guid UserId { get; set; }
        public string UserName { get; set; }
        public string Email { get; set; }
        public string Name { get; set; }
        public bool IsRemoved { get; set; }
    }

    public class SampleDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo = typeof(SampleDbContext).GetMethod(nameof(ConfigureGlobalFilters), BindingFlags.Instance | BindingFlags.NonPublic);

        public SampleDbContext(DbContextOptions<SampleDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                ConfigureGlobalFiltersMethodInfo
                    .MakeGenericMethod(entityType.ClrType)
                    .Invoke(this, new object[] { modelBuilder, entityType });
            }

            base.OnModelCreating(modelBuilder);
        }

        protected void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType) where TEntity : class
        {
            if (entityType.BaseType != null || !ShouldFilterEntity<TEntity>(entityType)) return;
            var filterExpression = CreateFilterExpression<TEntity>();
            if (filterExpression == null) return;
            if (entityType.IsQueryType)
                modelBuilder.Query<TEntity>().HasQueryFilter(filterExpression);
            else
                modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
        }

        protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
        {
            return typeof(IRemovable).IsAssignableFrom(typeof(TEntity));
        }

        protected Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>() where TEntity : class
        {
            Expression<Func<TEntity, bool>> expression = null;

            if (typeof(IRemovable).IsAssignableFrom(typeof(TEntity)))
            {
                Expression<Func<TEntity, bool>> removedFilter = e => !((IRemovable)e).IsRemoved;
                expression = expression == null ? removedFilter : CombineExpressions(expression, removedFilter);
            }

            return expression;
        }

        protected Expression<Func<T, bool>> CombineExpressions<T>(Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            return ExpressionCombiner.Combine(expression1, expression2);
        }
    }
}

DbContext’i bu şekilde düzenlediğimiz zaman artık elle IRemovable için elle tekrardan tanıtma işlemi yapmamıza gerek kalmamıştır.

Yazıda bahsi geçen uygulamanın kaynak kodlarına https://github.com/mennan/efcore-globalqueryfilters-sample adresinden ulaşabilirsiniz.

Visual Studio Code API’da Decorators Kullanımı

Okuma Süresi: 3 dakika

Visual Studio Code, kod editörüne tasarımsal olarak bazı eklemeler veya düzenlemeler için bize Decorator API isminde bir ortam sunmaktadır. Örnek olması açısından Visual Studio Code’da en çok kullanılan eklentilerden biri olan GitLens‘in kod editörüne yaptığı değişikliği gösterebiliriz.

Bu yazımda biz de buna benzer bir eklenti geliştireceğiz. Geliştireceğimiz eklenti o an açık olan dökümanda Console.WriteLine içeren ifadeleri kırmızı arkaplan ve beyaz yazı rengi ile vurgulayacak.

Daha önce Visual Studio Code için eklenti geliştirmemiş olanlar için https://code.visualstudio.com/api/get-started/your-first-extension adresindeki yazıyı okumalarını tavsiye ederim.

Eklenti oluşturmak için terminal ekranımızda aşağıdaki komutu yazmamız gerekmektedir.

yo code

Bu komutu yazdıktan sonra bize bazı sorular sorulacaktır. Geliştirme dili olarak JavaScript‘i, paket yöneticisi olarak olarak ise yarn‘ı seçtim. Gerekli bilgileri girip onayladıktan sonra eklenti projemiz oluşacak ve gerekli paketlerin kurulumları tamamlanacaktır.

İşlemler tamamlandıktan sonra oluşturulan klasörü Visual Studio Code ile açalım.

Eklenti, extension.js dosyasından başlayarak yüklenmeye başlayacaktır. Bunu değiştirmek isterseniz package.json dosyasındaki “main” bölümünü değiştirmeniz gerekmektedir. Biz şimdilik olduğu gibi bırakacağız.

package.json dosyasında geliştireceğimiz eklentinin ismi, açıklaması, kategorisi, geliştirici bilgileri, eklentide kullanılacak olan paketler gibi bilgiler bulunmaktadır. package.json dosyamızın içeriğini aşağıdaki şekilde değiştirelim.

{
  "name": "vscode-highlighter",
  "displayName": "vscode-highlighter",
  "description": "",
  "version": "0.0.1",
  "engines": {
    "vscode": "^1.33.0"
  },
  "categories": [
    "Other"
  ],
  "activationEvents": [
    "onCommand:extension.highlight"
  ],
  "main": "./extension.js",
  "contributes": {
    "commands": [
      {
        "command": "extension.highlight",
        "title": "Highlight"
      }
    ]
  },
  "scripts": {
    "postinstall": "node ./node_modules/vscode/bin/install",
    "test": "node ./node_modules/vscode/bin/test"
  },
  "devDependencies": {
    "typescript": "^3.3.1",
    "vscode": "^1.1.28",
    "eslint": "^5.13.0",
    "@types/node": "^10.12.21",
    "@types/mocha": "^2.2.42"
  }
}

Yaptığımız değişiklik ile Visual Studio Code üzerinden Highlight komutunu verdiğimiz zaman çalışacağınız belirttik.

Şimdi eklentimiz geliştirmeye başlamak için extension.js dosyasını açalım. Bu dosyayı açtığınızda sizi ilk olarak activate ve deactivate fonksiyonları karşılayacaktır.
active fonksiyonu eklenti komutu ilk defa çağrıldığında çalışacaktır.
deactivate fonksiyonu ise eklenti devre dışı bırakıldığı zaman çalışacaktır. Burayı kullanmış olduğunuz kaynakları temizlemek için kullanabilirsiniz.

Şimdi extension.js dosyasını aşağıdaki şekilde değiştirebiliriz.

// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const vscode = require("vscode");
const editor = vscode.window.activeTextEditor;
let decorationType = vscode.window.createTextEditorDecorationType({
  backgroundColor: "red",
  color: "white"
});
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  // Use the console to output diagnostic information (console.log) and errors (console.error)
  // This line of code will only be executed once when your extension is activated
  console.log(
    'Congratulations, your extension "vscode-highlighter" is now active!'
  );

  // The command has been defined in the package.json file
  // Now provide the implementation of the command with  registerCommand
  // The commandId parameter must match the command field in package.json
  let disposable = vscode.commands.registerCommand(
    "extension.highlight",
    function() {
      // The code you place here will be executed every time your command is executed

      let decorationsArray = [];
      let documentContent = editor.document.getText();
      let sourceCodeArr = documentContent.split("\n");

      for (let line = 0; line < sourceCodeArr.length; line++) {
        let text = sourceCodeArr[line];
        let match = /Console\.WriteLine\(".*?"\);/.exec(text);

        if (match !== null &amp;&amp; match.index !== undefined) {
          let range = new vscode.Range(
            new vscode.Position(line, match.index),
            new vscode.Position(line, match.index + match[0].length)
          );

          let decoration = {
            range: range
          };

          decorationsArray.push(decoration);
        }
      }

      editor.setDecorations(decorationType, decorationsArray);
    }
  );

  context.subscriptions.push(disposable);
}
exports.activate = activate;

// this method is called when your extension is deactivated
function deactivate() {}

module.exports = {
  activate,
  deactivate
};

25. satırda Visual Studio Code üzerinden Highlight komutu gönderildiği zaman eklentinin yapacağı işle ilgili bir fonksiyon tanımlıyoruz.

31. satır ile o an açık olan dökümanın içeriğini alıyoruz ve Regular Expression yardımıyla satırın içerisinde Console.WriteLine ifadesini arıyoruz. Eğer eşleşme olursa 5. satırda tanımlamasını yapmış olduğumuz decorator stilini ilgili alana uyguluyoruz. Decorator objesinin veri tipi DecorationOptions‘tır.

DecorationOption hakkında daha detaylı bilgi almak için https://code.visualstudio.com/api/references/vscode-api#DecorationOptions adresini ziyaret edebilirsiniz.

Eklentiyi test etmek için F5 tuşuna basmamız yeterlidir. F5 tuşuna bastıktan sonra ekrana Extension Development Host penceresi gelecektir. Eklentiyi denemek için Extension Development Host’ta F1 tuşuna basınız ve Highlight yazarak Enter tuşuna basınız. Döküman içerinde Console.WriteLine ifadesi varsa eklenti gerekli olan işlemi gerçekleştirecektir.

Yazıda bahsedilen eklentinin kaynak kodlarına https://github.com/mennan/vscode-decorator-sample adresinden erişebilirsiniz.