十、ASP.NET MVC 核心

在本章中,我们将涵盖以下主题:

  • 注入依赖关系并为控制器配置 IoC
  • 使用 ActionResults
  • 创建和处理区域
  • 创建和使用 POCO 控制器
  • 使用 MediatR 创建和使用控制器
  • 管理异常

注入依赖关系并为控制器配置 IoC

在本教程中,您将学习如何在控制器中使用构造函数注入依赖项,以及如何配置依赖项的生存期。

准备

我们用 VS 2017 创建了一个空的 web 应用,然后添加了一个空的控制器。 让我们创建一个存储库,将硬编码的值注入到控制器中。

怎么做……

我们已经讨论了用 ASP 注入依赖关系.NET Core第五章SOLID 原理,反转控制,依赖注入。 我们了解到 IoC 机制是 ASP 的内部机制.NET 的核心。 这是由构造函数完成的,它的生命周期必须在Startup.cs中的Configure方法中配置。 我们会做一些调整,一切都会自动工作。

  1. 首先,让我们看看要注入到控制器中的存储库:
public interface IProductRepository
{
  int GetCountProducts();
}
public class ProductRepository : IProductRepository
{
  public int GetCountProducts()
  {
    return 10;
  }
}

正如我们所看到的,这个存储库只有一个方法来检索字符串列表。

  1. 接下来,让我们将这个存储库注入到控制器构造函数中。 在ProductController.cs中,我们将开发以下代码:
public class ProductController : Controller
{
private readonly IProductRepository repo;
  public ProductController(IProductRepository repo)
  {
    this.repo = repo;
  }
  public IActionResult Index()
  {
    var count = repo.GetCountProducts();
    return View(new ValueViewModel(count));
  }
}
  1. 现在我们将创建一个ViewModel类和View文件:
public class ValueViewModel
{
  public ValueViewModel(object val)
  {
    Value = val.ToString();
  }
  public string Value { get; set; }
}

Index.cshtml代码如下:

@model Ch10.R1.ValueViewModel
<h1>Products count @Model.Value</h1>
  1. 接下来,我们将在Startup.csConfigureServices方法中配置生命周期存储库:
public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  services.AddTransient<IProductRepository, ProductRepository>();
}
  1. 最后,我们可以看到在操作方法中显示从存储库中检索的数据而没有错误的视图。 这是因为 ASP 的 IoC 内部机制.NET Core 自动为我们实例化回购,而不使用任何新的关键字,也不应用松耦合原则。
  2. We can also inject this repository by inserting the FromServices attribute above the repository used as a parameter of an action method in ProductController, after having configured it in ConfigureServices.

    我们需要对Startup.cs做如下更改:

public void ConfigureServices(IServiceCollection services)
{
  services.AddMvc();
  services.AddScoped<IProductRepository, ProductRepository>();
}

让我们将ProductController.cs文件修改如下:

public class ProductController : Controller
{
  public IActionResult Index([FromServices]IProductRepository repo)
  {
    var count = repo.GetCountProducts();
    return View();
  }
}
  1. 我们可以阅读下面的代码,并看到相同的 repo 已经通过第三方 IoC 容器注入到相同的控制器中,而不是通过内部 ASP.NET Core IoC 默认容器。

我们将创建一个IoConfig类来配置新的 IoC 容器。 当然,IoC 容器可以是 ASP 的任何现有 IoC 容器.NET Core 兼容 Unity、Ninject、Castle Windsor、StructureMap、SimpleInjector 等。 在这个例子中,我们将使用一个Autofac模块,根据 Autofac 文档:

A small class that can be used to bundle up a set of related components behind a facade to simplify configuration and deployment.

让我们按如下方式创建AutofacModule.cs文件:

public class AutofacModule : Module
{
  protected override void Load(ContainerBuilder builder)
  {
    builder.Register(c => new ProductRepository())
    .As<IProductRepository>()
    .InstancePerLifetimeScope();
  }
}

我们应该更改Startup.cs文件并注册Autofac模块:

using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using System;
public class Startup
{
  public IServiceProvider ConfigureServices
  (IServiceCollection services)
  {
    services.AddMvc();
    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder.RegisterModule<AutofacModule>();
    containerBuilder.Populate(services);
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
  }
}

另外,我们应该修改ProductController.cs文件:

public class ProductController : Controller
{
  private readonly IProductRepository repo;
  public ProductController(IProductRepository repo)
  {
    this.repo = repo;
  }
  public IActionResult Index()
  {
    var count = repo.GetCountProducts();
    return View();
  }
}

In any class where we use IoC, we have to use only the constructor where we inject dependencies. Adding a default empty constructor will generate an exception, because the IoC container will wait for only one construction with the dependencies to inject.

  1. 现在让我们看看Startup.cs中的ConfigureServices方法代码,它仍然带有Autofac,但没有模块:
public class Startup
{
  public IServiceProvider ConfigureServices
  (IServiceCollection services)
  {
    services.AddMvc();
    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder
    .RegisterType<ProductRepository>()
    .As<IProductRepository>();
    containerBuilder.Populate(services);
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
  }
}
  1. ASP.NET Core 自动将控制器注册并解析为服务; 但是,如果我们用 ASP 代替。 带有Autofac的 NET Core DI 容器(或另一个 DI 容器),ConfigureServices中的代码应更改如下:
public class Startup
{
  public IContainer AppContainer { get; private set; }
  public IServiceProvider ConfigureServices
  (IServiceCollection services)
  {
    services
    .AddMvc()
    .AddApplicationPart(typeof(ProductController).Assembly)
    .AddControllersAsServices();
    // Add Autofac
    var containerBuilder = new ContainerBuilder();
    containerBuilder
    .RegisterType<ProductRepository>()
    .As<IProductRepository>();
    containerBuilder.Populate(services);
    this.AppContainer = containerBuilder.Build();
    return new AutofacServiceProvider(this.AppContainer);
  }
}
  1. 我们可能想要处理在应用容器中已解析的资源。 要做到这一点,我们必须通过向Configure方法添加以下代码来注册ApplicationStopped事件:
public void Configure(IApplicationBuilder app,
IApplicationLifetime appLifetime)
{
  app.UseMvc(routes =>
  {
    routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");
  });
  appLifetime.ApplicationStopped.Register(() =>
  this.ApplicationContainer.Dispose());
}

使用 ActionResults

在本食谱中,您将了解我们将在遵循 mvc 的应用中使用哪些ActionResults

准备

我们用 VS 2017 创建了一个空的 web 应用,并添加了一个空的控制器。 我们将创建一个存储库,其中包含要注入控制器的硬编码值。

怎么做……

以下是所有类型,我们可以从控制器操作返回的特定的ActionResults:

public virtual JsonResult Json(object data)

public virtual ViewResult View()

public virtual ViewComponentResult ViewComponent(string componentName)

public virtual PartialViewResult PartialView()

public virtual ChallengeResult Challenge()

public virtual ForbidResult Forbid()

public virtual SignInResult SignIn(ClaimsPrincipal principal, string authenticationScheme)

public virtual SignOutResult SignOut(params string[] authenticationSchemes)

public virtual ContentResult Content(string content)

public virtual FileContentResult File(byte[] fileContents, string contentType)

public virtual FileStreamResult File(Stream fileStream, string contentType)

public virtual VirtualFileResult File(string virtualPath, string contentType)

public virtual FileStreamResult File(Stream fileStream, string contentType, string fileDownloadName)

public virtual LocalRedirectResult LocalRedirect(string localUrl);

public virtual PhysicalFileResult PhysicalFile(string physicalPath, string contentType)

public virtual RedirectResult Redirect(string url)

public virtual RedirectToActionResult RedirectToAction(string actionName)

public virtual RedirectToRouteResult RedirectToRoute(object routeValues)

创建和处理区域

在本食谱中,您将学习如何在 ASP 中管理区域.NET 的核心。 为此,我们将:

  • 创建领域
  • 创建区域路线
  • 避免区域航线冲突
  • 更改默认视图位置
  • 为区域的动作控制器创建链接

准备

我们在 VS 2017 中创建了一个空的 web 应用。

怎么做……

在构建 MVC 应用时,有时我们需要功能分离。 我们开发的应用可能比看起来的要大,这样我们在一个应用中有几个应用。 区域使我们能够根据需要创建许多 MVC 结构,并使我们能够更轻松地管理复杂的应用。 例如,在电子商务应用中,我们可以为网站的管理部分需要不同的区域,这些区域对应于不同的角色(用户管理、营销、汽车搜索参考、订单跟踪、库存管理),当然还有网站本身。

每个区域都有自己的控制器、模型和视图文件夹,我们必须在Startup.cs中配置该区域的路由,以匹配区域结构的物理文件路径与传入 URL。

没有像以前版本的 MVC 那样的脚手架来创建区域。 所以,我们必须从零开始创建区域:

  1. 让我们创建一个空 ASP.NET Core Web 应用在 VS 2017。
  2. 接下来,我们将右键单击应用根目录,并创建该区域的结构,其中包含一个 MVC 结构文件夹,如下面的截图所示。 我们还可以添加一个经典的 MVC 结构来讨论与区域的潜在路由冲突:

  1. 接下来,让我们为区域和控制器创建路由,在Startup.csConfigure方法中添加如下代码:
app.UseMvc(routes =>
{
  routes.MapRoute(
  name: "area",
  template: "{area=Products}/{controller=Home}/{action=Index}");
  routes.MapRoute(
  name: "default",
  template: "{controller=Home}/{action=Index}/{id?}");
});
  1. 如果我们使用Ctrl+F5来启动应用,我们将看到以下页面:

  1. 对于旧版本的 ASP。 在 asp.net MVC 中,我们将在MapRoute方法中为每个路由定义添加namespaces,以避免同名控制器之间的冲突。 ASP.NET Core MVC 中,我们更愿意使用每个控制器上面的 routing 属性来指定控制器来自某个区域,例如:
public class HomeController : Controller
{
  [Area("Products")]
  [Route("[area]/[controller]/[action]")]
  public IActionResult Index()
  {
    return View();
  }
}
  1. 为了访问Product区域中的Home控制器,我们必须在浏览器的地址工具栏中显式地输入区域路由:

  1. 如果我们想要更改AreaViewLocation,我们必须将以下代码添加到Startup.csConfiguresServices:
services.Configure<RazorViewEngineOptions>(options =>
{
  options.AreaViewLocationFormats.Clear();
  options.AreaViewLocationFormats.Add(
  "/Products/{2}/Views/{1}/{0}.cshtml");
  options.AreaViewLocationFormats.Add(
  "/Products/{2}/Views/Shared/{0}.cshtml");
  options.AreaViewLocationFormats.Add("/Views/Shared/{0}.cshtml");
});

We could also recreate the area's location by overriding RazorViewEngine.

  1. 要生成基于区域的链接,我们可以使用HtmlHelperTagHelper语法:
@Html.ActionLink("Products Area Home Page",
"Index", "Home", new { area = "Products" })
<a asp-area="Products" asp-controller="Home" asp-action="Index">Products Area Home Page</a>

创建和使用 POCO 控制器

在本食谱中,您将了解什么是 POCO 控制器,以及我们为什么要使用它们。

准备

我们在 VS 2017 中创建了一个空的 web 应用。

怎么做……

POCO 控制器是带有两个属性的简单类:

  • [Controller]属性,用于类本身或其基类
  • 一个routing属性,在应用中定义其路由

它们不继承自Controller类,因此它们将不能返回任何 MVC 或 WebAPI 结果作为视图、HttpStatus代码或任何继承自IActionResult的类型。

我们可以将它们放在应用的任何位置,项目的根目录,或者名为POCOController的文件夹中。 在这个练习中,我们将创建一个类,并通过在它上面添加Controller属性,按照给定的步骤创建一个控制器:

  1. 我们将添加Microsoft.AspNetCore.MvcNuget 包到项目:
"Microsoft.AspNetCore.Mvc": "2.0.0"
  1. 我们将添加 MVC 服务,并在Startup.cs中使用它:
public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddMvc();
  }
  public void Configure(IApplicationBuilder app)
  {
    app.UseMvc();
  }
}
  1. 现在,让我们添加一个POCO控制器:
[Controller]
[Route("api/[controller]")]
public class PocoCtrl
{
  [HttpGet]
  public string Get()
  {
    return "This is a POCO Controller";
  }
}
  1. 让我们看看这个控制器是否工作:

  1. 现在,让我们创建另一个POCO控制器,通过创建一个由controller装饰的基类和routing属性装饰的基类,作为一个继承自基类但没有装饰的简单类:
[Controller]
public class PocoCtrlBase { }
[Route("api/[controller]")]
public class PocoCtrlInherits : PocoCtrlBase
{
  [HttpGet]
  public string Get()
  {
    return "This is a POCO Controller inherited";
  }
}
  1. 让我们看看控制器是否工作:

The goal of the POCO controller is to create a lightweight controller which includes only action methods.

使用 MediatR 创建和使用控制器

在本菜谱中,您将学习在没有服务和存储库的情况下使用 MVC 控制器的另一种方法。

This recipe could be applied to the WebAPI controller. We could add that it's logical to mix MVC and WebAPI practices in the same controller. Now, there's no difference between them.

准备

我们在 VS 2017 中创建了一个空的 web 应用。

怎么做……

  1. 首先,让我们在project.json中添加MediatR 依赖注入包。 它将包括 MediatR 4.0.0 包:
"MediatR.Extensions.Microsoft.DependencyInjection": "4.0.0"

我们还需要AutoMapper包将Business models映射到ViewModels。 我们将在第 13 章ViewsModelsand ViewModels中更详细地讨论AutoMapper:

"AutoMapper.Extensions.Microsoft.DependencyInjection": "3.2.0"
  1. 接下来,让我们在Startup.cs中添加一些配置:
public class Startup
{
  public void ConfigureServices(IServiceCollection services)
  {
    var connection = @"Data Source=MyServer;Initial
    Catalog=CookBook;Integrated Security=True";
    services.AddDbContext<CookBookContext>(
    options => options.UseSqlServer(connection));
    services.AddMvc();
    services.AddAutoMapper(StartupAssembly());
    services.AddMediatR(StartupAssembly());
  }
  public void Configure(IApplicationBuilder app)
  {
    app.UseDeveloperExceptionPage();
    app.UseMvc(routes =>
    {
      routes.MapRoute(
      name: "default",
      template: "{controller=Book}/{action=Index}/{id?}");
    });
  }
  private static Assembly StartupAssembly()
  {
    return typeof(Startup).GetTypeInfo().Assembly;
  }
}

我们添加了 MVC、实体框架、Automapper 和 MediatR 作为 ASP 的服务.NET Core 应用。 我们现在可以使用它们。

  1. 接下来是DbContextBusiness对象:
public class CookBookContext : DbContext
{
  public CookBookContext(DbContextOptions<CookBookContext> options)
  : base(options) { }
  public DbSet<Book> Book { get; set; }
}
public class Book
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}
  1. 现在,让我们创建用于读取数据的Query对象。 我们将使用 MediatR 中的两个类来完成此工作:一个是继承自IAsyncRequestQuery类,另一个是继承自IAsyncRequestQueryHandlerHandler类。 这个Handler类实现了IAsyncRequestQueryHandler中的Handle方法,该方法接受Query类作为参数,并返回ViewModel以显示。 下面是这些类的代码:
    • ViewModels:
public class BookIndexViewModel
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}
public class BookListIndexViewModel
{
  public List<BookIndexViewModel> BookList { get; set; }
  public string Message { get; set; }
}
public class BookListIndexQuery : IAsyncRequest<BookListIndexViewModel>{ }
public class BookListIndexQueryHandler : IAsyncRequestHandler<BookListIndexQuery, BookListIndexViewModel>
{
  private readonly CookBookContext _context;
  public BookListIndexQueryHandler(CookBookContext context)
  {
    _context = context;
  }
  public async Task<BookListIndexViewModel> 
  Handle(BookListIndexQuery query)
  {
    var books = await _context.Book.ToListAsync();
    var model = new BookListIndexViewModel
    {
      BookList = await _context.Book
      .ProjectTo<BookIndexViewModel>()
      .ToListAsync()
    };
    return model;
  }
}
  1. 现在,让我们来看看控制器的代码:
public class BookController : Controller
{
  private readonly IMediator _mediator;
  public BookController(IMediator mediator)
  {
    _mediator = mediator;
  }
  public async Task<IActionResult> Index(BookListIndexQuery query)
  {
    var model = await _mediator.SendAsync(query);
    return View(model);
  }
 }
  1. 接下来,我们将在Views | Book | Index.cshtml文件中创建视图:
@model Ch10.R5.MediatR.BookListIndexViewModel
@if (Model.BookList.Any())
{
  <div>Books available</div>
  foreach (var book in Model.BookList)
  {
    <div>Id : @book.Id</div>
    <div>Name : @book.Name</div>
    <div>Price : @book.Price</div>
  }
}
  1. 让我们看看结果:

  1. 现在,我们创建ViewModelCommandCommandHandler类,对数据库进行插入操作。
public class BookAddViewModel
{
  [Required(ErrorMessage = "A name is required for the book")]
  [StringLength(50, ErrorMessage =
  "The name of the book must not exceed 50 characters")]
  public string Name { get; set; }
  [Required(ErrorMessage = "A price is required for this book")]
  [RegularExpression(@"^d+(.d{1,2})?$")]
  [Range(0.1, 100)]
  public decimal Price { get; set; }
}
    • Command:
public class BookAddCommand : IAsyncRequest<Result>
{
  public string Name { get; }
  public decimal Price { get; }
}
public class BookAddCommandHandler : IAsyncRequestHandler<BookAddCommand, Result>
{
  private readonly CookBookContext _context;
  public BookAddCommandHandler(CookBookContext context)
  {
    _context = context;
  }
  public async Task<Result> Handle(BookAddCommand command)
  {
    var book = new Book
    {
      Name = command.Name,
      Price = command.Price
    };
    _context.Book.Add(book);
    await _context.SaveChangesAsync();
    var result = new Result { Success = true };
    return result;
  }
}
public class Result
{
  public bool Success { get; set; }
  public string ErrorMessage { get; set; }
}
  1. 让我们看看新控制器的代码:
public class BookController : Controller
{
  private readonly IMediator _mediator;
  private readonly IMapper _mapper;
  public BookController(IMediator mediator, IMapper mapper)
  {
    _mediator = mediator;\
    _mapper = mapper;
  }
  public async Task<IActionResult> Index(BookListIndexQuery query)
  {
    var model = await _mediator.SendAsync(query);
    return View(model);
  }
  [HttpPost]
  [ValidateAntiForgeryToken]
  public async Task<IActionResult> Add(BookAddViewModel model)
  {
    if (ModelState.IsValid)
    {
      var command = new BookAddCommand();
      command = _mapper
      .Map<BookAddViewModel, BookAddCommand>(model);
      var result = await _mediator.SendAsync(command);
      if (result.Success)
      {
        return RedirectToAction("Index");
      }
      ModelState.AddModelError(string.Empty, result.ErrorMessage);
    }
    return View(model);
  }
}
  1. 最后,我们可以在创建了_ViewImports.cshtml文件后将这段代码添加到View中:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<div>
  <form asp-controller="Book" asp-action="Add" method="post" role="form">
    <label>Book Name</label><input type="text" name="Name" /><br />
    <label>Book Price</label><input type="text" name="Price" /><br />
    <input type="submit" value="Add Book" />
  </form>
</div>
  1. 我们可以这样构造这个项目:

它是如何工作的…

MediatR是通过应用一些命令查询职责隔离(或分离)概念获得的中介模式的实现。 这个 NuGet 包是由 Jimmy Bogard 创建的,他也创建了 AutoMapper。

CQRS 将我们的代码分为两个不同的部分:命令和查询(读取代码和持久写入代码)。

以下是关于 CQRS 模式优势的概述:

  • 分离读和写:我们可以将这个概念传播到我们的数据库配置中,以便有一个更快的只读(可能是非规范化的)数据库用于读,另一个用于写。 问题将是同步两者,但有几种方法可以做到这一点。
  • 有了 Azure,在我们的软件架构中很容易将服务总线与消息系统结合起来。 在功能需求方面,您添加的责任越多,就越增加其可维护性; 然而,如果您决定不承担任何责任,那么您就会拥有一个更轻量的、可维护性更低的微服务架构。
  • CQRS 模式使我们的代码更具可读性、可测试性和可维护性。
  • 相反,它会降低生产力和可读性,并增加复杂性。

There is no good or bad design pattern (except anti-pattern). They have to resolve a problem, and match with our project requirements. So, we don't have to follow a pattern or an architectural style by mode, but analyze seriously the pros and the cons of the pattern we apply, and ask ourselves the question: Are they really relevant for my project?

在 MediatR 中,称为处理程序的类管理查询和命令消息。 我们不使用 CQRS 中使用的事件溯源或领域驱动设计概念。 查询类用于读取,命令类用于插入、更新和删除操作。 命令和查询有它们自己的 viewmodel,也有它们自己的处理程序类,以便与数据库、服务、第三方服务等进行通信。

MediatR 模式给我们带来以下结果:

  • 我们应用单一责任原则,让控制器专注于管理 HTTP 调用,而不是承担许多其他职责,比如查询数据库、在表中插入一行,等等。
  • 不再需要存储库来抽象数据存储,也不再需要服务层来放入控制器的逻辑中。 关注的分离更好。

管理异常

在本菜谱中,您将学习如何以一种原始的方式管理异常。

准备

我们在 VS 2017 中创建了一个空的 web 应用。

怎么做……

  1. 首先,我们将创建一个Result类。 这个类将被Service层返回。 它允许我们管理要记录的错误消息并显示到视图:
public class Result
{
  public bool IsSuccess { get; }
  public string SuccessMessageToLog { get; set; }
  public string ErrorToLog { get; }
  public string ErrorToDisplay { get; set; }
  public ErrorType? ErrorType { get; }
  public bool IsFailure => !IsSuccess;
  protected Result(bool isSuccess, string error, ErrorType? errorType)
  {
    if ((isSuccess && error != string.Empty) ||
    (isSuccess && !errorType.HasValue))
    throw new InvalidOperationException();
    if ((!isSuccess && error == string.Empty) ||
    (isSuccess && errorType.HasValue))
    throw new InvalidOperationException();
    IsSuccess = isSuccess;
    ErrorToLog = error;
  }
  public static Result Fail(string message)
  {
    return new Result(false, message, null);
  }
  public static Result Fail(ErrorType? errorType)
  {
    return new Result(false, string.Empty, errorType);
  }
  public static Result Fail(string message, ErrorType? errorType)
  {
    return new Result(false, message, errorType);
  }
  public static Result Ok()
  {
    return new Result(true, string.Empty, null);
  }
}
public enum ErrorType
{
  DatabaseIsOffline,
  CustomerAlreadyExists
}
  1. 接下来,让我们创建ProductProductContextProductInputViewModel,我们将在后面使用:
public class Product
{
  public int Id { get; set; }
  public string Name { get; set; }
  public decimal Price { get; set; }
}
public class ProductContext : DbContext
{
  public ProductContext(DbContextOptions<ProductContext> options) : base(options){ }
  public DbSet<Product> Product { get; set; }
}
public class ProductInputViewModel
{
  public string Name { get; set; }
  public decimal Price { get; set; }
}
  1. 接下来,我们将添加ProductService类,其中我们将使用Result类来管理可能发生的异常,以及记录并显示到用户界面的消息:
public class ProductService : IProductService
{
  private readonly ILogger _logger;
  private readonly ProductContext _context;
  public ProductService(ILogger logger, ProductContext context)
  {
    _logger = logger;
    _context = context;
  }
  public Result CreateProduct(ProductInputViewModel productViewModel)
  {
    var product = new Product()
    {
      Name = productViewModel.Name,
      Price = productViewModel.Price
    };
    Result result = SaveProduct(product);
    if (result.IsFailure && result.ErrorType.HasValue)
    {
      switch (result.ErrorType.Value)
      {
        case ErrorType.DatabaseIsOffline:
        Log(result);
        result.ErrorToDisplay = "Unable to connect to the
        database. Please try again later";
        break;
        case ErrorType.CustomerAlreadyExists:
        Log(result);
        result.ErrorToDisplay = "A product with the name " +
        productViewModel.Name + " already exists";
        break;
        default:
        throw new ArgumentException();
      }
    }
    return result;
  }
  private Result SaveProduct(Product product)
  {
    try
    {
      _context.Product.Add(product);
      _context.SaveChanges();
      return Result.Ok();
    }
    catch (DbUpdateException ex)
    {
      if (ex.Message == "Unable to open the DB connection")
      return Result.Fail(ex.Message,
      ErrorType.DatabaseIsOffline);
      if (ex.Message.Contains("IX_Customer_Name"))
      return Result.Fail(ex.Message,
      ErrorType.CustomerAlreadyExists);
      throw;
    }
  }
  private void Log(Result result)
  {
    if (result.IsFailure)
    _logger.LogError(result.ErrorToLog);
    else
    _logger.LogInformation(result.SuccessMessageToLog);
  }
}
  1. 最后添加ProductController:
public class ProductController : Controller
{
  private readonly IProductService _service;
  public ProductController(IProductService service)
  {
    _service = service;
  }
  [HttpGet]
  public ActionResult Index()
  {
    return View();
  }
  [HttpPost]
  public ActionResult CreateProduct(ProductInputViewModel product)
  {
    Result productResult = _service.CreateProduct(product);
    if (productResult.IsFailure)
    {
      ModelState.AddModelError(string.Empty,
      productResult.ErrorToDisplay);
      return View();
    }
    return RedirectToAction("Index");
  }
}