返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
Asp-Net-Core学习笔记

Asp-Net-Core Study Notes: 2. MVC Views, Models, Persistence, Files, Error Handling, Logging

TagHelper Getting Started Advantages: Automatically generated based on parameters, no need to write hyperlinks by hand, similar to the url command in Django templates. Add TagHelper in ViewImport @addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers For example, using the link TagHelper a class=btn btn-outline-primary asp-controller=st

曦远 (DealiAxy)约 44 分钟
目录30 个章节

TagHelper

Getting Started

Advantages: Automatically generated based on parameters, no need to write hyperlinks by hand, similar to the url command in Django templates.

Add TagHelper in ViewImport

@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers

For example, using the link TagHelper

<a class="btn btn-outline-primary" 
   asp-controller="student" asp-action="get" 
   asp-route-id="@student.Id">
    查看
</a>

Cache-busting TagHelper

<img src="~/images/banner.jpg" alt="Alternate Text" asp-append-version="true" />

Environment TagHelper

Use local CSS files in the development environment, and CDN CSS files in non-development environments.

Note: integrity is used for integrity checking, to ensure the completeness and security of files provided by the CDN.

<environment include="Development">
    <link href="~/lib/twitter-bootstrap/css/bootstrap.css" rel="stylesheet" />
</environment>

<environment exclude="Development">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</environment>

To prevent the page from failing to display when the CDN fails to load, you can add fallback-related attributes. The first is the file to load on failure, and the second skips integrity checking for that file.

asp-fallback-href="~/lib/twitter-bootstrap/css/bootstrap.css"
asp-suppress-fallback-integrity="true"

Form Tag Helper

I'll just paste some layout code, with the class styles removed, keeping only the most basic code.

It really is quite convenient, and it holds its own against templates like Django and jinja2.

@model Student

<form asp-controller="student" asp-action="create">
    <label asp-for="Name"></label>
    <input asp-for="Name" />

    <label asp-for="Email"></label>
    <input asp-for="Email" />

    <label asp-for="ClassName"></label>
    <select asp-for="ClassName" asp-items="Html.GetEnumSelectList<ClassNameEnum>()"></select>

    <button type="submit">提交</button>
</form>

Model Binding

The order in which data from the HTTP request is bound to the corresponding parameters on the controller method:

  • Form Values (POST form data)
  • Route Values (values in the route)
  • Query String (GET query string)

Model Validation

1. Configure the model

First add validation attributes to the Model, for example:

public class Student
{
    public int Id { get; set; }

    [Display(Name = "姓名"), MaxLength(4, ErrorMessage = "名字长度不能超过四个字")]
    [Required(ErrorMessage = "请输入名字!")]
    public string Name { get; set; }

    [Display(Name = "班级")]
    public ClassNameEnum ClassName { get; set; }

    [Required(ErrorMessage = "请输入邮箱!")]
    [Display(Name = "邮箱")]
    [RegularExpression(@"^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$", ErrorMessage = "邮箱格式不正确")]
    public string Email { get; set; }
}
Common model validation methods
  • Required
  • Range: specify the allowed minimum and maximum values
  • MinLength
  • MaxLength
  • Compare: compare two properties, such as password and confirm password
  • RegularExpression: regular expression matching

2. Add validation code in the controller

Use ModelState.IsValid to validate whether the model properties are correct

[HttpPost]
public IActionResult Create(Student student)
{
    if (ModelState.IsValid)
    {
        var newStudent = _studentRepository.Add(student);
        return RedirectToAction("details", new { id = newStudent.Id });
    }

    return View();
}

3. Use TagHelper to display error messages on the page

Example as follows:

<div class="text-danger" asp-validation-summary="All"></div>

<div class="form-group row">
<label asp-for="Name" class="col-sm-2 col-form-label"></label>
<div class="col-sm-10">
<input asp-for="Name" class="form-control" />
<span class="text-danger" asp-validation-for="Name"></span>
</div>
</div>

Getting Started with EF Core

First, implement DbContext

public class AppDbContext:DbContext
{
    // 将应用程序的配置传递给DbContext
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    // 对要使用到的每个实体都添加 DbSet<TEntity> 属性
    // 通过DbSet属性来进行增删改查操作
    // 对DbSet采用Linq查询的时候,EFCore自动将其转换为SQL语句
    public DbSet<Student> Students { get; set; }
}

Register the DbContext connection pool

services.AddDbContextPool<AppDbContext>(options => options.UseSqlServer(_configuration.GetConnectionString("StudentDBConnection")));

The local SQL Server database configuration is in appserttings.json:

"ConnectionStrings": {
    "StudentDBConnection": "server=(localdb)\\MSSQLLocalDB;database=StudentDB;Trusted_Connection=true"
}

Implement the repository

public class SqlStudentRepository : IStudentRepository
{
    private readonly AppDbContext _context;

    public SqlStudentRepository(AppDbContext context)
    {
        _context = context;
    }
    public Student Add(Student student)
    {
        _context.Students.Add(student);
        _context.SaveChanges();
        return student;
    }
    public Student Delete(int id)
    {
        var student = _context.Students.Find(id);
        if (student != null)
        {
            _context.Students.Remove(student);
            _context.SaveChanges();
        }
        return student;
    }
    public IEnumerable<Student> GetAll() => _context.Students;
    public Student GetById(int id) => _context.Students.Find(id);
    public Student Update(Student updatedStudent)
    {
        var student = _context.Students.Attach(updatedStudent);
        student.State = EntityState.Modified;
        _context.SaveChanges();
        return updatedStudent;
    }
}

Common EF Core commands

In the NuGet console, these are case-insensitive

  • Get-Help about_enti: show help; the full name of about_enti is very long, so you can just write the beginning
  • Add-Migration: add a migration
  • Update-Database: update the database

If using the dotnet CLI, please install it first: dotnet tool install --global dotnet-ef

  • dotnet ef migrations add initial: add a migration
  • dotnet ef database update: update the database

For more operations, you can refer to the official documentation: https://docs.microsoft.com/en-us/ef/core/cli/dotnet

Add seed data

Override the OnModelCreating method of DbContext

protected override void OnModelCreating(ModelBuilder modelBuilder) { 
    modelBuilder.Entity<Student>().HasData(
        new Student { Id = 1, Name = "小米" },
    );
}

To keep the DbContext code from getting too messy, you can also use an extension method:

public static class ModelBuilderExtensions
{
    public static void InsertSeedData(this ModelBuilder mBuilder)
    {
        mBuilder.Entity<Student>().HasData(
            new Student { Id = 1, Name = "小米" },
        );
    }
}

Domain model and database schema

  • Use migrations to keep the domain model and database schema in sync
  • Use add-migration to add a migration
  • Use remove-migration to delete the most recent record
  • Use update-database migration-name to roll back to any migration

File Upload

Define the ViewModel

Use the IFormFile type for the field to be uploaded

public class StudentCreateViewModel
{
    public int Id { get; set; }
	// 省略无关代码...
    [Display(Name = "图片")]
    public IFormFile Photo { get; set; }
}

Write the view

Modify the cshtml view file and change the model binding:

@model StudentCreateViewModel

Add a form field for file upload

<div class="form-group row">
    <label asp-for="Photo" class="col-sm-2 col-form-label"></label>
    <div class="col-sm-10">
        <div class="custom-file">
        <input asp-for="Photo" class="form-control custom-file-input" />
        <label class="custom-file-label">请选择图片</label>
        </div>
    </div>
</div>

To show the file name after a file is selected, you also need to write some JS:

$(document).ready(function () {
    $('.custom-file-input').on('change', function () {
        var fileName = $(this).val().split('\\').pop();
        $(this).next('.custom-file-label').html(fileName);
    });
});

Write the controller

Inject HostingEnvironment via the constructor

public StudentController(IStudentRepository studentRepository, HostingEnvironment hostingEnvironment)
{
    _studentRepository = studentRepository;
    _hostingEnvironment = hostingEnvironment;
}

Logic for handling file upload and saving

[HttpPost]
public IActionResult Create(StudentCreateViewModel model)
{
    if (ModelState.IsValid)
    {
        var uniqueFileName = "";
        if (model.Photo != null)
        {
            var uploadDir = Path.Combine(
                _hostingEnvironment.WebRootPath, 
                "uploads", "images");
            uniqueFileName = Guid.NewGuid().ToString() + 
                "_" + model.Photo.FileName;
            model.Photo.CopyTo(new FileStream(
                Path.Combine(uploadDir, uniqueFileName), 
                FileMode.Create));
        }
        var student = new Student
        {
            Name = model.Name,
            Email = model.Email,
            ClassName = model.ClassName,
            PhotoPath = uniqueFileName,
        };

        var newStudent = _studentRepository.Add(student);
        return RedirectToAction("details", 
                                new { id = newStudent.Id });
    }

    return View();
}

Multiple File Upload

Pretty much the same as with a single file

ViewModel

Add a List<IFormFile> typed field

[Display(Name = "图库")]
public List<IFormFile> Gallery { get; set; }

Modify the View

<div class="form-group row">
    <label asp-for="Gallery" class="col-sm-2 col-form-label"></label>
    <div class="col-sm-10">
        <div class="custom-file">
            <input asp-for="Gallery" multiple id="gallery-input" class="form-control custom-file-input" />
            <label id="gallery-label" class="custom-file-label">请选择图片 可以一次选择多张</label>
        </div>
    </div>
</div>

JS code:

$('#gallery-input').on('change', function () {
    var label = $(this).next('#gallery-label');
    var files = $(this)[0].files;
    if (files.length > 1) {
        label.html(`你已经选择了${files.length}个文件`);
    } else if (files.length == 1) {
        label.html(files[0].name);
    }
});

Modify the Controller Code

// 处理多文件上传
if (model.Gallery != null && model.Gallery.Count > 0)
{
    foreach(var photo in model.Gallery)
    {
        uniqueFileName = Guid.NewGuid().ToString() + "_" 
            + photo.FileName;
        photo.CopyTo(new FileStream(Path.Combine(uploadDir, 
        uniqueFileName),FileMode.Create));
    }
}

Error Handling

Add an Error Handling Page

Configure the middleware in Startup.cs:

app.UseStatusCodePagesWithReExecute("/error/{0}");

It's recommended to use UseStatusCodePagesWithReExecute rather than UseStatusCodePagesWithRedirects. The former executes the error jump URL within the pipeline, while the latter redirects to that URL, causing the HTTP error status code to become the 200 status code of the newly executed page.

Then write the error controller:

public class ErrorController : Controller
{
    [Route("Error/{statusCode}")]
    public IActionResult Index(int statusCode)
    {
        var statusCodeResult = HttpContext.Features.
            Get<IStatusCodeReExecuteFeature>();
        var viewModel = new ErrorViewModel
        {
            Path = statusCodeResult.OriginalPath,
            QueryString = statusCodeResult.
                OriginalQueryString,
        };
        switch (statusCode)
        {
            case 404:
                viewModel.Message = "页面未找到";
                break;
        }
        return View("Error", viewModel);
    }
}

Oh right, I also defined a ViewModel:

public class ErrorViewModel
{
    public int Code { get; set; }
    public string Message { get; set; }
    public string Path { get; set; }
    public string QueryString { get; set; }
}

I won't paste the view code; it just displays these error messages from the ViewModel~

Set Up Global Exception Redirect

Add the middleware

app.UseExceptionHandler("/exception");

Write the handler controller. Here you need to add the AllowAnonymous attribute, allowing users to access this exception page even when not logged in, ensuring the exception page can be displayed no matter what.

[AllowAnonymous]
[Route("exception")]
public IActionResult ExceptionHandler()
{
    var exception = HttpContext.Features.
        Get<IExceptionHandlerPathFeature>();
    var viewModel = new ExceptionViewModel
    {
        Path = exception.Path,
        Message = exception.Error.Message,
        StackTrace = exception.Error.StackTrace,
    };
    return View("Exception", viewModel);
}

Also, the ViewModel is defined as follows:

public class ExceptionViewModel
{
    public string Path { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }
}

Logging

ASP.NET Core comes with a built-in logging system, which is already registered in the service container by default. You can use it just by injecting it in the controller's constructor, for example:

public class ErrorController : Controller
{
    private ILogger<ErrorController> _logger;

    public ErrorController(ILogger<ErrorController> logger)
    {
        this._logger = logger;
    }
}

By default, logs are only written to the console or debug output. However, to achieve more functionality, such as writing to a file or pushing to a log server, we need to use a third-party logging component. Here I'm using NLog.

First, you need to install the NLog.Web.AspNetCore package.

Then add the NLog service in Program.cs:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .ConfigureLogging((hostingContext, logging) =>
     {
         // 保留官方的代码中的默认日志程序
 logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
         logging.AddConsole();
         logging.AddDebug();
         logging.AddEventSourceLogger();
         // 引入 nlog
         logging.AddNLog();
      }).UseStartup<Startup>();

For the part about keeping the official default logging providers, you need to look at the ASP.NET Core source code. This article uses version 2.2, which you can view on GitHub at the following address:

https://github.com/dotnet/aspnetcore/blob/v2.2.8/src/DefaultBuilder/src/WebHost.cs

Then, to use NLog, you need to create a configuration file. Create NLog.config in the project root directory:

For documentation on the configuration file, you can refer to: https://github.com/NLog/NLog/wiki

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true"
      throwConfigExceptions="true">
  <targets>
    <target name="f1" xsi:type="File" fileName="Logs\nlog-all-${shortdate}.log"/>
    <target name="n1" xsi:type="Network" address="tcp://localhost:4001"/>
    <target name="c1" xsi:type="Console" encoding="utf-8"
            error="true"
            detectConsoleAvailable="true" />
    <target name="c2" xsi:type="ColoredConsole" encoding="utf-8"
          useDefaultRowHighlightingRules="true"
          errorStream="true"
          enableAnsiOutput="true"
          detectConsoleAvailable="true"
          DetectOutputRedirected="true">
    </target>
  </targets>
  <rules>
    <logger name="*" maxLevel="Debug" writeTo="c2" />
    <logger name="*" minLevel="Info" writeTo="f1" />
  </rules>
</nlog>

After that, you can use the logging functionality normally in the program. For example:

var viewModel = new StatusCodeViewModel
{
    Code = statusCode,
    Path = statusCodeResult.OriginalPath,
    QueryString = statusCodeResult.OriginalQueryString,
};
_logger.LogWarning(viewModel.ToString());

You can also configure log levels and namespace filtering in appsettings.json, which has the same effect as configuring them in NLog.conf. For example:

"Logging": {
    "LogLevel": {
        "Default": "Warning",
        "StudyManagement.Controllers.ErrorController": 
        "Warning"
    }
},

Welcome to Connect

Programming Lab focuses on exploring popular new internet technologies and team agile development practices. Reply with linux, flutter, c#, netcore, android, kotlin, java, python, etc. in the backend of the WeChat official account "Programming Lab" to get related technical articles and materials. If you have any questions, you can also leave a message in the official account backend~

_EOF_

Asp-Net-Core Study Notes: 2. MVC Views, Models, Persistence, Files, Error Handling, Logging

本文作者:
程序设计实验室
版权声明:
本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关文章

Asp-Net-Core学习笔记:部署,早知道,还是docker,以及一点碎碎念

前言 AspNetCore技术栈在我们团队里的使用也有一段时间了,之前的部署方式一直是本地编译之后上传可执行文件到服务器,使用supervisor来管理进程这种很原始的方式。 参考之前的文章: 对于小项目来说尚可,够用,但是存在几个问题: 每次更新花费的时间太长了,无论是Framework-Dependent还是Self-Contained,都要上传很大的文件~ 更新的时候需要在superviso

2022-02-16 · Asp-Net-Core学习笔记 · 约 1 分钟

Asp-Net-Core学习笔记:单元测试和集成测试

我在使用 AspNetCore 的这段时间内,看了很多开源项目和博客,发现各种 .Net 体系的新技术很多人都有关注和使用,但却很少有人关注测试。 测试是软件生命周期中的一个非常重要的阶段,对于保证软件的可靠性具有极其重要的意义。在应用程序的开发过程中,为了确保它的功能与预期一致,必须对其进行测试。这样做不仅能够确保功能正确执行,同时能够帮助开发人员尽早地发现并改正系统中所存在的缺陷(Bug),

2023-07-17 · Asp-Net-Core学习笔记 · 约 1 分钟

Asp-Net-Core学习笔记:WebApi开发实践

前言 用AspNetCore做Api开发也有一段时间了,正好年底在做总结,做一个WebApi开发实践笔记。 暂时想到的一些技术关键词,同时也作为本文的大纲,现在对这套技术体系的了解还不够深入,以后会持续更新这个Api开发实践~ 身份认证:JwtBearer 分页:X.PagedList 缓存 ResponseCache Redis:StackExchange.Redis 数据持久化 / ORM F

2022-07-04 · Asp-Net-Core学习笔记 · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…