65.9K
CodeProject 正在变化。 阅读更多。
Home

Entity Framework 中的行级别安全性

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (9投票s)

2016年8月4日

CPOL

5分钟阅读

viewsIcon

14545

在 C# 和 Entity Framework 中实现行级别安全性

引言

在本文中,我们将实现行级别安全,它的解决方案使我们的应用程序能够进行重构,并且此功能使我们能够轻松管理项目。

并且这种方式将减少你的业务代码,如果你使用 DDD,可以显著减少服务中的代码。

总的来说,有多种方法可以实现这个解决方案,但最简单的方法之一是编写可用的接口,并在你的通用存储库上使用它们的验证。

背景

假设我们有两个模型:“User”和“Post”。现在,我想创建一个具有专业功能的通用存储库。

它的存储库可以自动选择预期用户的 Posts 记录,此外,如果该用户是管理员,则应该看到所有帖子。

我们可以通过在自定义存储库中使用行级别安全轻松实现此功能。

使用代码

现在,我们开始编码,我使用 Visual Studio IDE,我将创建一个名为“Console1”的控制台应用程序,它将如下所示

首先,我们需要使用 NuGet 包管理器下载 Entity Framework。

之后,创建一个名为“Models”的文件夹,并在其中创建一个名为“User”的类。

它将是这样的

using System;

namespace Console1.Models
{
    public enum UserType
    {
        Admin,
        Ordinary
    }
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public int Age { get; set; }

        public UserType Type { get; set; }

    }
}
 

我认为 UserType 是完全显而易见的,它只是一个枚举,在这个实体中扮演一个角色。

并且另一个字段也具有合理性。

现在,让我们实现“IUser”接口,我们在 Models 文件夹中创建它,它将是这样的

namespace Console1.Models
{
    public interface IUser
    {
        int UserId { get; set; }

        User User { get; set; }
    }
}

每个与 User 有关联的实体都必须实现 IUser 接口。

现在在这个文件夹中也创建一个名为“Post”的另一个类,并将代码放在那里...

using System.ComponentModel.DataAnnotations.Schema;

namespace Console1.Models
{
    public class Post : IUser
    {
        public int Id { get; set; }

        public string Context { get; set; }

        public int UserId { get; set; }

        [ForeignKey(nameof(UserId))]
        public User User { get; set; }

    }
}

它们的关系是一对多,这意味着每个用户可以有 N 篇帖子。

够了,现在我们要将这些类引入 EF 上下文,所以我们在 Models 文件夹中创建一个名为“Context”的类。

using System.Data.Entity;

namespace Console1.Models
{
    public class Context : DbContext
    {
        public Context() : base("Context")
        {
        }

        public DbSet Users { get; set; }

        public DbSet Posts { get; set; }
    }
}    

我们明确指定了我们有两个 Post 和 User 类型的 DbSet,这意味着我们的数据库中将有两张表,当然,我们的数据库名称将是“Context”。

到这里,我们已经完成了这个项目的第一步,现在我们可以使用 Migration 创建一个数据库,并向其中添加一些初始记录。

所以,在包管理器控制台中输入“Enable-migrations”

它会自动创建一个名为“Migrations”的文件夹,并在其中创建一个名为“Configuration.cs”的类。

现在像下面这样修改 Configuration.cs

namespace Console1.Migrations
{
    using Models;
    using System.Data.Entity.Migrations;

    internal sealed class Configuration : DbMigrationsConfiguration
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
        }

        protected override void Seed(Console1.Models.Context context)
        {

            context.Users.AddOrUpdate(x => x.Id,
              new User { Id = 1, Name = "aaa", Age = 30, Type = UserType.Admin },
              new User { Id = 2, Name = "bbb", Age = 20, Type = UserType.Ordinary },
              new User { Id = 3, Name = "ccc", Age = 25, Type = UserType.Ordinary }
            );

            context.Posts.AddOrUpdate(x => x.Id,
                new Post { Context = "ccc 1", UserId = 3 },
                new Post { Context = "bbb 1", UserId = 2 },
                new Post { Context = "bbb 2", UserId = 2 },
                new Post { Context = "aaa 1", UserId = 1 },
                new Post { Context = "bbb 3", UserId = 2 },
                new Post { Context = "ccc 2", UserId = 3 },
                new Post { Context = "ccc 3", UserId = 3 }
            );

            context.SaveChanges();
        }
    }
}

在“Seed”方法中,我们告诉这些是我们的初始记录(仅作示例)

在你的控制台中输入“Update-database”并发送该命令,数据库将自动创建并包含这些初始记录。

上面这些项目都很清楚,并且写下来是因为我们在本文的其余部分需要它们。

当我们创建 Repository 时,我们需要上面所有的项目。

 

现在我想开始本文的主要部分,即支持行级别安全的存储库。

在项目根目录下创建一个名为“Repository”的文件夹,并在其中创建一个名为“GenericRepository”的类。

你的项目结构应该如下所示

像这样实现 Generic Repository

using Console1.Models;
using System;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;

namespace Console1.Repository
{
    public interface IGenericRepository<T>
    {
        IQueryable<T> CustomizeGet(Expression<Func<T, bool>> predicate);
        void Add(T entity);
        IQueryable<T> GetAll();
    }

    public class GenericRepository<TEntity, DbContext> : IGenericRepository<TEntity>
        where TEntity : class, new() where DbContext : Models.Context, new()
    {

        private DbContext _entities = new DbContext();

        public IQueryable<TEntity> CustomizeGet(Expression<Func<TEntity, bool>> predicate)
        {
            IQueryable<TEntity> query = _entities.Set<TEntity>().Where(predicate);
            return query;
        }

        public void Add(TEntity entity)
        {
            int userId = Program.UserId; // fake UserId

            if (typeof(IUser).IsAssignableFrom(typeof(TEntity)))
            {
                ((IUser)entity).UserId = userId;
            }

            _entities.Set<TEntity>().Add(entity);
        }

        public IQueryable<TEntity> GetAll()
        {
            IQueryable<TEntity> result = _entities.Set<TEntity>();

            int userId = Program.UserId; // fake UserId


            if (typeof(IUser).IsAssignableFrom(typeof(TEntity)))
            {
                User me = _entities.Users.Single(c => c.Id == userId);
                if (me.Type == UserType.Admin)
                {
                    return result;
                }
                else if (me.Type == UserType.Ordinary)
                {
                    string query = $"{nameof(IUser.UserId).ToString()}={userId}";

                    return result.Where(query);
                }
            }
            return result;
        }
        public void Commit()
        {
            _entities.SaveChanges();
        }
    }
}

以上代码的描述

1) 我们有一个名为 IGenericRepository 的泛型接口,GenericRepository 类将实现它。

2) 接口包含一个名为 CustomizeGet 的方法,该方法有一个 predicate 参数,并且只返回它的可查询(这只是一个示例,不包含在本文中),以及“Add”和“GetAll”方法,这些方法应该直接实现我们的行级别安全目标。

3) GenericRepositoryClass 接受两个泛型类型,并且它的类实现了 IGenericRepository,并且显然已声明 TEntity 是一个类,DbContext 是我们之前实现的 Context 类型。

4) 你可以看到 CustomizeGet 的实现,它创建了一个相关的查询并返回它。

5) “Add”方法通过一个 TEntity 类型的参数(应保存的模型)来实现,之后你看到我通过硬编码方式为 UserId 分配了一个数字,当然你知道对于这项工作,如果你开发 Web 应用程序,你应该使用类似 Asp.Net Identity 的东西,并返回一个之前声明的已认证的 userId。

并且通过 IsAssignableFrom,我们指定了 TEntity 是否是 IUser!如果条件为真,我们将 UserId 添加到 TEntity,例如,在你的服务中,你不需要添加这个重复的字段,在下一级,我们将把它添加到 TEntity。

6) 最有趣的是名为“GetAll”的方法,首先我们创建一个该实体的查询,然后在下一级指定它是否是 IUser!如果条件为真,它将找到预期的用户,如果它的类型是管理员,则该方法返回所有集合(管理员可以访问所有帖子),如果用户的类型是“普通”,则使用动态 LINQ 创建预期的查询,并创建一个条件,使得 UserId 等于 userId。在这种情况下,普通用户将只看到他/她的所有帖子。

注意:要下载 dynamic linq,你可以在 nuget 包管理器中搜索“Dynamic.linq”。

如果 TEntity 不是 IUser,我们将返回结果,这意味着我们不想在稍微宽松的模型中进行行级别安全,创建的查询有能力返回所有记录。

7) 当然,在数据库中执行保存操作。

 

我们在“Seed”中创建了一些初始记录,现在我们想运行并测试所有这些过程。我们应该在项目根目录下打开“Program.cs”并像这样实现它

using System;
using Console1.Models;
using Console1.Repository;
using System.Collections.Generic;
using System.Linq;

namespace Console1
{
    public class Program
    {
        public static int UserId = 1; //fake userId
        static void Main()
        {
            GenericRepository repo = new GenericRepository();

            List posts = repo.GetAll().ToList();

            foreach (Post item in posts)
                Console.WriteLine(item.Context);

            Console.ReadKey();
        }
    }
}

正如你所见,UserId 是硬编码的,我们可以静态地更改它。并且数据库中创建的内容是,例如,UserId = 1 对应管理员,其他用户类型为普通。

在 Main 方法中,我们创建了一个 GenericRepository 的实例,并通过使用“GetAll”方法并列出它,返回所有预期的记录并打印它。

我们期望 userId 等于一,返回所有 posts 记录,因为该用户是管理员。

现在,只需静态地更改 UserId,例如将其更改为 2 并运行项目,操作将发生变化,你将看到一个普通用户只能看到他/她的帖子。

你应该看到类似这样的内容

你看到,仅仅通过更改 UserId,我们应用程序的行为就会完全改变,并且只会返回用户的帖子。

结论

同样的方式,你可以实现和设计复杂的存储库,它可以处理和管理各种验证。

你可以在我的 Github 账户下载所有代码。

© . All rights reserved.