【asp.net core 系列】9 实战之 UnitOfWork以及自定义代码生成

0. 前言

在前一篇中我们创建了一个基于EF的数据查询接口实现基类,这一篇我将带领大家讲一下为这EF补充一些功能,并且提供一个解决避免写大量配置类的方案。

1. SaveChanges的外移

在之前介绍EF Core的时候,我们提到过使用EF需要在每次使用之后,调用一次SaveChanges将数据提交给数据库。在实际开发中,我们不能添加一条数据或者做一次修改就调用一次SaveChanges,这完全不现实。因为每次调用SaveChanges是EF向数据库提交变更的时候,所以EF推荐的是每次执行完用户的请求之后统一提交数据给数据库。

这样就会造成一个问题,可能也不是问题:我们需要一个接口来管理EF 的SaveChanges操作。

1.1 创建一个IUnitOfWork接口

通常我们会在Domain项目中添加一个IUnitOfWork接口,这个接口有一个方法就是SaveChanges,代码如下:

namespace Domain.Insfrastructure
{
    public interface IUnitOfWork
    {
        void SaveChanges();
    }
}

这个方法的意思表示到执行该方法的时候,一个完整的工作流程执行完成了。也就是说,当执行该方法后,当前请求不会再与数据库发生连接。

1.2 实现IUnitOfWork接口

在 Domain.Implement中添加IUnitOfWork实现类:

using Domain.Insfrastructure;
using Microsoft.EntityFrameworkCore;

namespace Domain.Implements.Insfrastructure
{
    public class UnitOfWork: IUnitOfWork
    {
        private DbContext DbContext;
        public UnitOfWork(DbContext context)
        {
            DbContext = context;
        }

        public void SaveChanges()
        {
            DbContext.SaveChanges();
        }
    }
}

1.3 调用时机

到现在我们已经创建了一个UnitOfWork的方法,那么问题来了,我们该在什么时候调用呢,或者说如何调用呢?

我的建议是创建一个ActionFilter,针对所有的控制器进行SaveChanges进行处理。当然了,也可以在控制器中持有一个IUnitOfWork的示例,然后在Action结束的时候,执行SaveChanges。不过这样存在一个问题,可能会存在遗漏的方法。所以我推荐这样操作,这里简单演示一下如何创建拦截器:

在Web的根目录下,创建一个Filters目录,这个目录里用来存储一些过滤器,创建我们需要的过滤器:

using Domain.Insfrastructure;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Web.Filters
{
    public class UnitOfWorkFilterAttribute : ActionFilterAttribute
    {
        public IUnitOfWork UnitOfWork;

        public override void OnActionExecuted(ActionExecutedContext context)
        {
            UnitOfWork.SaveChanges();
        }
    }
}

使用一个ActionFilter可以很方便的解决一些容易遗漏但又必须执行的代码。这里就先不介绍如何配置Filter的启用和详细介绍了,请允许我卖个关子。当然了,有些小伙伴肯定也能猜到这是一个Attribute类,所以可以按照Attribute给Controller打标记。

2. 创建一个简单的代码生成方法

之前在介绍EF的时候,有个小伙伴跟我说,还要写配置文件啊,太麻烦了。是的,之前我介绍了很多关于写配置文件不使用特性的好处,但不解决这个问题就无法真正体检配置类的好处。

虽然说,EF Core约定优先,但是如果默认约定的话,得在DBContext中声明 DbSet<T> 来声明这个字段,实体类少的话,比较简单。如果多个数据表的话,就会非常麻烦。

所以这时候就要使用工具类, 那么简单的分析一下,这个工具类需要有哪些功能:

  • 第一步,找到实体类并解析出实体类的类名
  • 第二步,生成配置文件
  • 第三步,创建对应的Repository接口和实现类

很简单的三步,但是难点就是找实体类并解析出实体类名。

在Util项目中添加一个Develop目录,并创建Develop类:

namespace Utils.Develop
{
    public class Develop
    {
        
    }
}

定位当前类所在目录,通过

Directory.GetCurrentDirectory()

这个方法可以获取当前执行的DLL所在目录,当然不同的编译器在执行的时候,会有微妙的不同。所以我们需要以此为根据然后获取项目的根目录,一个简单的方法,查找*.sln 所在目录:

public static string CurrentDirect
{
    get
    {
        var execute = Directory.GetCurrentDirectory();
        var parent = Directory.GetParent(execute);
        while(parent.GetFiles("*.sln",SearchOption.TopDirectoryOnly).Length == 0)
        {
            parent = parent.Parent;
            if(parent == null)
            {
                return null;
            }
        }
        return parent.FullName;
    }
}

2.1 获取实体类

那么获取到根目录之后,我们下一步就是获取实体类。因为我们的实体类都要求是继承BaseEntity或者命名空间都是位于Data.Models下面。当然这个名称都是根据实际业务场景约束的,这里只是以当前项目举例。那么,我们可以通过以下方法找到我们设置的实体类:

public static Type[] LoadEntities()
{
    var assembly = Assembly.Load("Data");
    var allTypes = assembly.GetTypes();
    var ofNamespace = allTypes.Where(t => t.Namespace == "Data.Models" || t.Namespace.StartsWith("Data.Models."));
    var subTypes = allTypes.Where(t => t.BaseType.Name == "BaseEntity`1");
    return ofNamespace.Union(subTypes).ToArray();
}

通过 Assembly加载Data的程序集,然后选择出符合我们要求的实体类。

2.2 编写Repository接口

我们先约定Model的Repository接口定义在 Domain/Repository目录下,所以它们的命名空间应该是:

namespace Domain.Repository 
{
}

假设目录情况与Data/Models下面的代码结构保持一致,然后生成代码应该如下:

public static void CreateRepositoryInterface(Type type)
{
    var targetNamespace = type.Namespace.Replace("Data.Models", "");
    if (targetNamespace.StartsWith("."))
    {
        targetNamespace = targetNamespace.Remove(0);
    }
    var targetDir = Path.Combine(new[]{CurrentDirect,"Domain", "Repository"}.Concat(
        targetNamespace.Split('.')).ToArray());
    if (!Directory.Exists(targetDir))
    {
        Directory.CreateDirectory(targetDir);
    }

    var baseName = type.Name.Replace("Entity","");

    if (!string.IsNullOrEmpty(targetNamespace))
    {
        targetNamespace = $".{targetNamespace}";
    }
    var file = $"using {type.Namespace};\r\n"
        + $"using Domain.Insfrastructure;\r\n"
        + $"namespace Domain.Repository{targetNamespace}\r\n"
        + "{\r\n"
        + $"\tpublic interface I{baseName}ModifyRepository : IModifyRepository<{type.Name}>\r\n" +
        "\t{\r\n\t}\r\n"
        + $"\tpublic interface I{baseName}SearchRepository : ISearchRepository<{type.Name}>\r\n" +
        "\t{\r\n\t}\r\n}";

    File.WriteAllText(Path.Combine(targetDir, $"{baseName}Repository.cs"), file);
}

2.3 编写Repository的实现类

因为我们提供了一个基类,所以我们在生成方法的时候,推荐继承这个类,那么实现方法应该如下:

public static void CreateRepositoryImplement(Type type)
{
    var targetNamespace = type.Namespace.Replace("Data.Models", "");
    if (targetNamespace.StartsWith("."))
    {
        targetNamespace = targetNamespace.Remove(0);
    }

    var targetDir = Path.Combine(new[] {CurrentDirect, "Domain.Implements", "Repository"}.Concat(
        targetNamespace.Split('.')).ToArray());
    if (!Directory.Exists(targetDir))
    {
        Directory.CreateDirectory(targetDir);
    }
    var baseName = type.Name.Replace("Entity", "");
    if (!string.IsNullOrEmpty(targetNamespace))
    {
        targetNamespace = $".{targetNamespace}";
    }

    var file = $"using {type.Namespace};" +
        $"\r\nusing Domain.Implements.Insfrastructure;" +
        $"\r\nusing Domain.Repository{targetNamespace};" +
        $"\r\nusing Microsoft.EntityFrameworkCore;" +
        $"namespace Domain.Implements.Repository{targetNamespace}\r\n" +
        "{" +
        $"\r\n\tpublic class {baseName}Repository :BaseRepository<{type.Name}> ,I{baseName}ModifyRepository,I{baseName}SearchRepository " +
        "\r\n\t{" +
        $"\r\n\t\tpublic {baseName}Repository(DbContext context) : base(context)"+
        "\r\n\t\t{"+
        "\r\n\t\t}\r\n"+
        "\t}\r\n}";
    File.WriteAllText(Path.Combine(targetDir, $"{baseName}Repository.cs"), file);
}

2.4 配置文件的生成

仔细观察一下代码,可以发现整体都是十分简单的。所以这篇就不掩饰如何生成配置文件了,小伙伴们可以自行尝试一下哦。具体实现可以等一下篇哦。

3. 总结

这一篇初略的介绍了两个用来辅助EF Core实现的方法或类,这在开发中很重要。UnitOfWork用来确保一次请求一个工作流程,简单的代码生成类让我们能让我们忽略那些繁重的创建同类代码的工作。

更多内容烦请关注我的博客《高先生小屋》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268