大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。
当前系列: ASP.NET 修改讲义

AutoMapper

在ViewModel和entity转换时,大量的重复代码,比如:

return new _LogOnStatusModel
{
    Id = current.Id,
    UserName = current.UserName
};

可以使用工具简化。演示:

入门三部曲

使用AutoMapper只需要:(不同版本使用方法略有不同)
  1. 得到一个MapperConfiguration(映射配置)实例
    MapperConfiguration config = new MapperConfiguration(
        cfg => cfg.CreateMap<User, _LogOnStatusModel>()
        );
  2. 根据MapperConfiguration得到一个IMapper对象
    IMapper mapper = config.CreateMapper();
  3. 调用IMapper的Map()方法开始映射
    _LogOnStatusModel model = mapper.Map<_LogOnStatusModel>(current);
断点演示:通过映射,model获得了current的值

但是,注意:MapperConfiguration的生成是比较消耗资源的!而且整个项目只需要使用一个MapperConfiguration即可,怎么办?

架构

你可能想到了“单例模式”,但实际上不需要这么麻烦。

MapperConfiguration对象

在SRV的基类 BaseService 中引入静态构造函数,生成静态只读的MapperConfiguration对象即可:(复习为什么这样就行?

protected readonly static MapperConfiguration config;
static BaseService()
{
    config = new MapperConfiguration(
        cfg =>
        {
            cfg.CreateMap<User, _LogOnStatusModel>();
            cfg.CreateMap<User, _UserInfoModel>();

断点演示:不关闭IIS,哪怕是停止调试,都不会重新运行该静态构造函数

IMapper 

然后,在 BaseService 中引入一个 mapper 属性,供所有子类使用:

    protected IMapper mapper => config.CreateMapper(); 
mapper是“轻量级”的,可以直接使用,一般不需要特别处理。(估计AutoMapper内部也进行了相应的处理,不是每次CreateMapper()就真的new一个)

这样,在SRV的子类中就可以直接使用mapper了。

using优化

另外,根据我们PerViewPerModel的原则(复习),ViewModel的(短)命名可能大量重复,比如:

  • MVCSample.SRV.ViewModel.Article.SingleModel
  • MVCSample.SRV.ViewModel.Problem.SingleModel
  • MVCSample.SRV.ViewModel.Suggest.SingleModel
  • ……

@想一想@:这时候使用using管用不?

我们可以使用全名来区分,但也可以取个巧:

using VM = SRV.ViewModel.Models;
这样,下面就可以这样写:
cfg.CreateMap<VM.Article.SingleModel, Article>();
cfg.CreateMap<VM.Problem.SingleModel, Problem>();
cfg.CreateMap<VM.Suggest.SingleModel, Suggest>();

映射规则

AutoMapper可以自动的映射:

  1. 名称相同的属性
  2. 复合名称”相同的属性
  3. 已被配置映射的
    1. 复杂”属性
    2. 集合子元素

public class User : BaseEntity
{
    public string UserName { get; set; }
    public string Password { get; set; }
public class IndexModel
{
    public string UserName { get; set; }
    public string Password { get; set; }
1
    public User InvitedBy { get; set; }
    public string InvitedByUserName { get; set; }
2
    public User InvitedBy { get; set; }
    public IndexModel InvitedBy { get; set; }
3.1
    public IList<Comment> Comments { get; set; }
    public IList<CommentModel> Comments { get; set; }
3.2

如果名称不同,需要进行配置。比如,_LogOnStatusModel的Name属性,由User的UserName获得:

cfg.CreateMap<User, _LogOnStatusModel>()
    .ForMember(l => l.Name, opt => opt.MapFrom(u => u.UserName))

MapFrom()里的Func可以自由扩展,比如:

.ForMember(l => l.Name, opt => opt.MapFrom(u => u.UserName.Trim()))

还可以配置忽略某个属性。比如,_LogOnStatusModel的Password,不要映射(忽略之):

cfg.CreateMap<User, _LogOnStatusModel>()
    .ForMember(l => l.Password, opt => opt.Ignore())
以及,如果某个属性为null值时的处理方案(AutoMapper不会报NullReferenceException)。比如,如果_LogOnStatusModel的Name映射过后值为null的话,将其替换成“匿名用户”:
cfg.CreateMap<User, _LogOnStatusModel>()
    .ForMember(l => l.Name, opt => opt.NullSubstitute("匿名用户"))

一般来说,AutoMapper中只需要执行上述这些简单的“映射”逻辑,复杂逻辑不应该由MapFrom()实现。

最后,通过添加ReverseMap(),可以形成“双向”映射(之前的映射都是单向的)

cfg.CreateMap<VM.Register.IndexModel, User>().ReverseMap();

验证

默认的,AutoMapper只映射它能够映射的属性。

有时候我们希望AutoMapper能够检查是否“所有的属性”都可以映射,这就需要添加语句:

config.AssertConfigurationIsValid();

这样如果DestinationType中还有没有被映射的属性,AutoMapper就会抛出异常。(演示)

还可以在映射时添加一个参数MemberList,通过枚举指定:

  • 忽略这种检查
    cfg.CreateMap<User, _LogOnStatusModel>(MemberList.None)
  • 检查SourceType
    cfg.CreateMap<User, _LogOnStatusModel>(MemberList.Source)
  • 检查DestinationType(默认行为)

编辑

之前的mapper.Map(model)方法是利用传入的model,全新的new出来一个对象。

但有时候我们希望能利用model“修改”一个对象,比如:

public class User
{
    public int Id { get; set; }    //有Id
    public string UserName { get; set; }
public class IndexModel 
{
    //没有Id
    public string UserName { get; set; }
注意User有Id而IndexModel没有Id,这是前提。

接下来,User和IndexModel对象都是已有的:

User current = new User
{
    UserName = "atai",
    Id = 10
IndexModel model = new IndexModel
{
    UserName = "fg",

演示对比:

  • 全新生成的对象,覆盖了原有Id
    current = mapper.Map<User>(model);
  • current对象仍然被保留,Id没有被覆盖,但UserName被改变
    mapper.Map(model, current);

更多的特性,留待同学们自己去发掘……o(* ̄︶ ̄*)o


Autofac

复习:DI和IoC,现在Controller依赖Service……

演示:额外引入了MockService和ServiceInterface

#体会#:为什么需要接口?

在不断的开发迭代过程中,我们需要不断的MockServiceProdService之间进行切换,怎么才能方便的进行呢?

在C#中我们可以使用:

条件编译符

复习

        public ArticleController()
        {
#if UI
            articleService = new MockService.ArticleService();
#else
            articleService = new ProdService.ArticleService();
#endif
        }

但更优雅的方式是使用Autofac这种第三方依赖注入工具(官网地址

注册

因为其在MVC中非常流行,所以除了Autofac组件以外,还有专门的MVC integration组件:

通过nuget下载了上述两个Autofac类库之后,在Global.asax文件中,MvcApplication.cs的Application_Start()方法中:

  1. 获得一个ContainerBuilder(容器建造器)对象
    ContainerBuilder builder = new ContainerBuilder();
  2. 注册Controller和FilterProvider
    builder.RegisterControllers(typeof(MvcApplication).Assembly);
    builder.RegisterFilterProvider();

然后,指示使用何种interface/基类的实现:

  1. 可以一个类一个类的注册
    builder.RegisterType<MockService.UserService>().As<IUserService>();
  2. 也可以整个程序集的注册
    builder.RegisterAssemblyTypes(typeof(ProdService.UserService).Assembly)
                    .AsImplementedInterfaces();

注意:AsImplementedInterfaces()能注册(非接口)的基类,比如DbContext。

这里一样可以利用条件编译符:

#if Mock
using MockService;
#else
using ProdService;
#endif

注意:需要开发者自定义设置的就这点代码,其他的都只需要原样copy就是了。

通过ContainerBuilder得到一个IContainer容器对象,并为MVC自动设定解析(resolve:获取“接口对象”)
IContainer container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));

#体会#:Builder模式

注入

Controller

可以通过构造函数注入:

private IUserService userService;
public SharedController(IUserService userService)
{
    this.userService = userService;
}

#理解#:

  • MVC通过Autofac获得一个IUserService对象,将其传入SharedController的构造函数,“实例化”一个SharedController……
  • IUserService对象(SharedController的依赖)被注入

注意

  • 构造函数可以有多个参数(演示)
  • 这个构造函数的参数仍然是接口,而且也只能是接口,不能是实现类。
  • 不能再声明其他构造函数(为什么见后文)

Filter

如果是继承的Attribute,可以使用属性注入:(因为已经在Golbal.asax.cs中:builder.RegisterFilterProvider();

public class NeedLogOnAttribute : AuthorizeAttribute
{
    public IUserService Service { get; set; }
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        if (Service.GetLogonStatus() == null)

如果filter实现的是接口,就只能通过Container来Resolve():

DbContext context = AutofacDependencyResolver.Current.GetService<DbContext>();


IUserService service = AutofacConfig.Container.Resolve<IUserService>();//老版

优雅实现:ContextPerRequest

注意方法InstancePerRequest():

builder.RegisterAssemblyTypes(typeof(BaseService).Assembly).InstancePerRequest().AsSelf();
builder.RegisterType(typeof(MysqlDbContext)).InstancePerRequest().As<DbContext>();

另:AsSelf()在没有基类/接口时使用。


Elmah

是一个专门用于记录错误日志的第三方组件官网)。

理论上我们也可以自己记录(比如利用HandleErrorAttribute.OnException()使用log4net),但是……,你懂的,(^_-)

引入

MVC中应用,原文4步,实际上只需要2步:

  1. 添加引用(会自动修改web.config)
  2. 修改web.config,添加logger(类似log4.net)。以下演示为使用xml文件记录:
    <elmah>
      <errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/App_Data" />
    </elmah>
    即将log文件存入到根目录下/App_Data文件夹。

演示

  • 报错之后,App_Data下有错误日志文件
  • 浏览器中通过访问/elmah.axd可以看到“格式化”(整理过后)的报错信息页面

权限和过滤

出于安全考虑(比如cookie消息可能会被泄露等),elmah默认禁止了远程访问/elmah.axd。

演示访问:https://17bang.ren/elmah.axd,403禁止访问。

这是在web.config中有配置的:

<security allowRemoteAccess="false" />

PS:不要放开权限(不建议),确实需要对特定人员放开权限的,可以配置authorization。但这需要配合ASP.NET自带的权限管理机制才能生效,我们没讲……

另外,还可以进行错误过滤(某些异常不进行处理),可以在Global.asax中添加:
//和Application_Start()并列,由Elmah自行调用
void ErrorLog_Filtering(object sender, ExceptionFilterEventArgs e)
{
    //通过e.Exception可以获得诸多有用信息,比如Message、StackTrace等……
    if (e.Exception.GetBaseException() is HttpRequestValidationException)
    {
        e.Dismiss();   //忽略当前异常


错误页面

无论是出于安全,还是“用户友好”的考虑,我们都不能把包含源代码/堆栈信息的“报错页面(黄页)”直接呈现给用户的。

customErrors

在web.config中开启“自定义错误页面”配置:

<customErrors mode="On"></customErrors>

默认是Off,所以有报错就直接黄页。

还可以设置RemoteOnly:只在远程访问(非localhost,浏览器和ASP.NET项目不在同一地址)时使用自定义的报错页面。这样,开发/维护人员就可以在项目发布以后,还可以登陆远程服务器,查看到黄页中的报错信息。

HandleErrorAttribute

MVC默认在~/App_Start/FilterConfig.cs下注册了HandleErrorAttribute。

当customErrors开启生效之后,程序运行异常就会呈现(不是重定向)默认的错误页面内容:~/Views/Shared/Error.cshtml(演示)

注意错误页面全是静态内容。这样可以避免“循环错误”,即:自定义错误页面上面还有报错!

redirect

HandleErrorAttribute是不会处理404错误的(因为此时根本就没有进入Controller/Action可控的Filter领域)。

404错误的处理需要在customErrors中配置:

<error statusCode="404" redirect="/404.html"/>  
即:404的错误重定向到/404.html页面。

注意:404.html是一个静态HTML页面,直接放在项目根目录下,而不是要放在Views下。

customErrors的配置效力在HandleErrorAttribute之后,所以500的配置不会生效:

<error statusCode="500" redirect="/500.html"/>

除非注释掉HandleErrorAttribute的注册代码……(演示

报错页面

重定向到自定义错误页面后,MVC会自动在url后面加上参数aspxerrorpath,如:


作业

  1. 使用AutoMapper重构之前文章发布/修改/单页的作业。
  2. 利用Autofac,切换到使用MockService,能在文章单页下显示评论列表。
  3. 有序组织错误页面,比如:
    1. 请求不存在的文章单页,转到404页面
    2. 修改其他用户文章的,转到500页面
  4. 配置elmah,能记录项目所有异常。
学习笔记
源栈学历
大多数人,都低估了编程学习的难度,而高估了自己的学习能力和毅力。

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ASP.NET 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码