使用静态基类方案让 ASP.NET Core 实现遵循 HATEOAS Restful Web API

Hypermedia As The Engine Of Application State (HATEOAS)

HATEOAS(Hypermedia as the engine of application state)是 REST 架构风格中最复杂的约束,也是构建成熟 REST 服务的核心。它的重要性在于打破了客户端和服务器之间严格的契约,使得客户端可以更加智能和自适应,而 REST 服务本身的演化和更新也变得更加容易。

HATEOAS的优点有:

具有可进化性并且能自我描述

超媒体(Hypermedia, 例如超链接)驱动如何消费和使用API, 它告诉客户端如何使用API, 如何与API交互, 例如: 如何删除资源, 更新资源, 创建资源, 如何访问下一页资源等等. 

例如下面就是一个不使用HATEOAS的响应例子:

{    "id" : 1,    "body" : "My first blog post",    "postdate" : "2015-05-30T21:41:12.650Z"}

如果不使用HATEOAS的话, 可能会有这些问题:

  • 客户端更多的需要了解API内在逻辑

  • 如果API发生了一点变化(添加了额外的规则, 改变规则)都会破坏API的消费者.

  • API无法独立于消费它的应用进行进化.

如果使用HATEOAS:

{

    "id" : 1,

    "body" : "My first blog post",

    "postdate" : "2015-05-30T21:41:12.650Z",

    "links" : [

        {

            "rel" : "self",

            "href" : http://blog.example.com/posts/{id},

            "method" : "GET"

        },

     {

        "rel": "update-blog",

       "href": http://blog.example.com/posts/{id},

        "method" "PUT"

        }

        ....

    ] 

}

这个response里面包含了若干link, 第一个link包含着获取当前响应的链接, 第二个link则告诉客户端如何去更新该post. 

Roy Fielding的一句名言: "如果在部署的时候客户端把它们的控件都嵌入到了设计中, 那么它们就无法获得可进化性, 控件必须可以实时的被发现. 这就是超媒体能做到的." ????

比如说针对上面的例子, 我可以在不改变响应主体结果的情况下添加另外一个删除的功能(link), 客户端通过响应里的links就会发现这个删除功能, 但是对其他部分都没有影响.

所以说HTTP协议还是很支持HATEOAS的:

如果你仔细想一下, 这就是我们平时浏览网页的方式. 浏览网站的时候, 我们并不关心网页里面的超链接地址是否变化了, 只要知道超链接是干什么就可以.

我们可以点击超链接进行跳转, 也可以提交表单, 这就是超媒体驱动应用程序(浏览器)状态的例子.

如果服务器决定改变超链接的地址, 客户端程序(浏览器)并不会因为这个改变而发生故障, 这就浏览器使用超媒体响应来告诉我们下一步该怎么做.

那么怎么展示这些link呢? 

JSON和XML并没有如何展示link的概念. 但是HTML却知道, anchor元素: 

<a href="uri" rel="type"  type="media type">

href包含了URI

rel则描述了link如何和资源的关系

type是可选的, 它表示了媒体的类型

为了支持HATEOAS, 这些形式就很有用了:

{

    ...

    "links" : [

        {

            "rel" : "self",

            "href" : http://blog.example.com/posts/{id},

            "method" : "GET"

        }

        ....

    ] 

}

method: 定义了需要使用的方法

rel: 表明了动作的类型

href: 包含了执行这个动作所包含的URI.

 

为了让ASP.NET Core Web API 支持HATEOAS, 得需要自己手动编写代码实现. 有两种办法:

静态类型方案: 需要基类(包含link)和包装类, 也就是返回的资源的ViewModel里面都含有link, 通过继承于同一个基类来实现.

动态类型方案: 需要使用例如匿名类或ExpandoObject等, 对于单个资源可以使用ExpandoObject, 而对于集合类资源则使用匿名类.

这一篇文章介绍如何实施第一种方案 -- 静态类型方案

首先需要准备一个asp.net core 2.0 web api的项目. 项目搭建的过程就不介绍了, 我的很多文章里都有介绍.

下面开始建立Domain Model -- Vehicle.cs:

using SalesApi.Core.Abstractions.DomainModels;


namespace SalesApi.Core.DomainModels

{

    public class Vehicle: EntityBase

    {

        public string Model { get; set; }

        public string Owner { get; set; }

    }

}

这里的父类EntityBase是我的项目特有的, 您可能不需要.

然后为这个类添加约束(数据库映射的字段长度, 必填等等) VehicleConfiguration.cs:

using Microsoft.EntityFrameworkCore.Metadata.Builders;

using SalesApi.Core.Abstractions.DomainModels;


namespace SalesApi.Core.DomainModels

{

    public class VehicleConfiguration : EntityBaseConfiguration<Vehicle>

    {

        public override void ConfigureDerived(EntityTypeBuilder<Vehicle> b)

        {

            b.Property(x => x.Model).IsRequired().HasMaxLength(50);

            b.Property(x => x.Owner).IsRequired().HasMaxLength(50);

        }

    }

}

然后把Vehicle添加到SalesContext.cs:

using Microsoft.EntityFrameworkCore;

using SalesApi.Core.Abstractions.Data;

using SalesApi.Core.DomainModels;


namespace SalesApi.Core.Contexts

{

    public class SalesContext : DbContextBase

    {

        public SalesContext(DbContextOptions<SalesContext> options)

            : base(options)

        {

        }


        protected override void OnModelCreating(ModelBuilder modelBuilder)

        {

            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfiguration(new ProductConfiguration());

            modelBuilder.ApplyConfiguration(new VehicleConfiguration());

            modelBuilder.ApplyConfiguration(new CustomerConfiguration());

        }


        public DbSet<Product> Products { get; set; }

        public DbSet<Vehicle> Vehicles { get; set; }

        public DbSet<Customer> Customers { get; set; }

    }

}

建立IVehicleRepository.cs:

using SalesApi.Core.Abstractions.Data;

using SalesApi.Core.DomainModels;


namespace SalesApi.Core.IRepositories

{

    public interface IVehicleRepository: IEntityBaseRepository<Vehicle>

    {

        

    }

}

这里面的IEntityBaseRepository也是我项目里面的类, 您可以没有.

然后实现这个VehicleRepository.cs:

using SalesApi.Core.Abstractions.Data;

using SalesApi.Core.DomainModels;

using SalesApi.Core.IRepositories;


namespace SalesApi.Repositories

{

    public class VehicleRepository : EntityBaseRepository<Vehicle>, IVehicleRepository

    {

        public VehicleRepository(IUnitOfWork unitOfWork) : base(unitOfWork)

        {

        }

    }

}

具体的实现是在我的泛型父类里面了, 所以这里没有代码, 您可能需要实现一下.

然后是重要的部分:

建立一个LinkViewMode.cs 用其表示超链接:

namespace SalesApi.Core.Abstractions.Hateoas

{

    public class LinkViewModel

    {

        public LinkViewModel(string href, string rel, string method)

        {

            Href = href;

            Rel = rel;

            Method = method;

        }

        

        public string Href { get; set; }

        public string Rel { get; set; }

        public string Method { get; set; }

    }

}

里面的三个属性正好就是超链接的三个属性.

然后建立LinkedResourceBaseViewModel.cs, 它将作为ViewModel的父类:

using System.Collections.Generic;

using SalesApi.Core.Abstractions.DomainModels;


namespace SalesApi.Core.Abstractions.Hateoas

{

    public abstract class LinkedResourceBaseViewModel: EntityBase

    {

        public List<LinkViewModel> Links { get; set; } = new List<LinkViewModel>();

    }

}

这样一个ViewModel就可以包含多个link了.

然后就可以建立VehicleViewModel了:

using SalesApi.Core.Abstractions.DomainModels;

using SalesApi.Core.Abstractions.Hateoas;


namespace SalesApi.ViewModels

{

    public class VehicleViewModel: LinkedResourceBaseViewModel

    {

        public string Model { get; set; }

        public string Owner { get; set; }

    }

}

注册Repository:

services.AddScoped<IVehicleRepository, VehicleRepository>();

注册Model/ViewModel到AutoMapper:

CreateMap<Vehicle, VehicleViewModel>();CreateMap<VehicleViewModel, Vehicle>();

建立VehicleController.cs:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.JsonPatch;

using Microsoft.AspNetCore.Mvc;

using Microsoft.EntityFrameworkCore;

using SalesApi.Core.Abstractions.Hateoas;

using SalesApi.Core.DomainModels;

using SalesApi.Core.IRepositories;

using SalesApi.Core.Services;

using SalesApi.Shared.Enums;

using SalesApi.ViewModels;

using SalesApi.Web.Controllers.Bases;


namespace SalesApi.Web.Controllers

{

    [AllowAnonymous]

    [Route("api/sales/[controller]")]

    public class VehicleController : SalesBaseController<VehicleController>

    {

        private readonly IVehicleRepository _vehicleRepository;

        private readonly IUrlHelper _urlHelper;


        public VehicleController(

            ICoreService<VehicleController> coreService,

            IVehicleRepository vehicleRepository,

            IUrlHelper urlHelper) : base(coreService)

        {

            _vehicleRepository = vehicleRepository;

            this._urlHelper = urlHelper;

        }


        [HttpGet]

        [Route("{id}", Name = "GetVehicle")]

        public async Task<IActionResult> Get(int id)

        {

            var item = await _vehicleRepository.GetSingleAsync(id);

            if (item == null)

            {

                return NotFound();

            }

            var vehicleVm = Mapper.Map<VehicleViewModel>(item);

            return Ok(CreateLinksForVehicle(vehicleVm));

        }


        [HttpPost]

        public async Task<IActionResult> Post([FromBody] VehicleViewModel vehicleVm)

        {

            if (vehicleVm == null)

            {

                return BadRequest();

            }


            if (!ModelState.IsValid)

            {

                return BadRequest(ModelState);

            }


            var newItem = Mapper.Map<Vehicle>(vehicleVm);

            _vehicleRepository.Add(newItem);

            if (!await UnitOfWork.SaveAsync())

            {

                return StatusCode(500, "保存时出错");

            }


            var vm = Mapper.Map<VehicleViewModel>(newItem);


            return CreatedAtRoute("GetVehicle", new { id = vm.Id }, CreateLinksForVehicle(vm));

        }


        [HttpPut("{id}", Name = "UpdateVehicle")]

        public async Task<IActionResult> Put(int id, [FromBody] VehicleViewModel vehicleVm)

        {

            if (vehicleVm == null)

            {

                return BadRequest();

            }


            if (!ModelState.IsValid)

            {

                return BadRequest(ModelState);

            }

            var dbItem = await _vehicleRepository.GetSingleAsync(id);

            if (dbItem == null)

            {

                return NotFound();

            }

            Mapper.Map(vehicleVm, dbItem);

            _vehicleRepository.Update(dbItem);

            if (!await UnitOfWork.SaveAsync())

            {

                return StatusCode(500, "保存时出错");

            }


            return NoContent();

        }


        [HttpPatch("{id}", Name = "PartiallyUpdateVehicle")]

        public async Task<IActionResult> Patch(int id, [FromBody] JsonPatchDocument<VehicleViewModel> patchDoc)

        {

            if (patchDoc == null)

            {

                return BadRequest();

            }

            var dbItem = await _vehicleRepository.GetSingleAsync(id);

            if (dbItem == null)

            {

                return NotFound();

            }

            var toPatchVm = Mapper.Map<VehicleViewModel>(dbItem);

            patchDoc.ApplyTo(toPatchVm, ModelState);


            TryValidateModel(toPatchVm);

            if (!ModelState.IsValid)

            {

                return BadRequest(ModelState);

            }


            Mapper.Map(toPatchVm, dbItem);


            if (!await UnitOfWork.SaveAsync())

            {

                return StatusCode(500, "更新时出错");

            }


            return NoContent();

        }


        [HttpDelete("{id}", Name = "DeleteVehicle")]

        public async Task<IActionResult> Delete(int id)

        {

            var model = await _vehicleRepository.GetSingleAsync(id);

            if (model == null)

            {

                return NotFound();

            }

            _vehicleRepository.Delete(model);

            if (!await UnitOfWork.SaveAsync())

            {

                return StatusCode(500, "删除时出错");

            }

            return NoContent();

        }

private VehicleViewModel CreateLinksForVehicle(VehicleViewModel vehicle)

        {

            vehicle.Links.Add(

                new LinkViewModel(

                    href: _urlHelper.Link("GetVehicle", new { id = vehicle.Id }),

                    rel: "self",

                    method: "GET"));


            vehicle.Links.Add(

                new LinkViewModel(

                    href: _urlHelper.Link("UpdateVehicle", new { id = vehicle.Id }),

                    rel: "update_vehicle",

                    method: "PUT"));


            vehicle.Links.Add(

            new LinkViewModel(

                href: _urlHelper.Link("PartiallyUpdateVehicle", new { id = vehicle.Id }),

                rel: "partially_update_vehicle",

                method: "PATCH"));


            vehicle.Links.Add(

            new LinkViewModel(

                href: _urlHelper.Link("DeleteVehicle", new { id = vehicle.Id }),

                rel: "delete_vehicle",

                method: "DELETE"));


            return vehicle;

        }

    }

}

在Controller里, 查询方法返回的都是ViewModel, 我们需要为ViewModel生成Links, 所以我建立了CreateLinksForVehicle方法来做这件事.

假设客户通过API得到一个Vehicle的时候, 它可能会需要得到修改(整体修改和部分修改)这个Vehicle的链接以及删除这个Vehicle的链接. 所以我把这两个链接放进去了, 当然别忘了还有本身的链接也一定要放进去, 放在最前边.

这里我使用了IURLHelper, 它会通过Action的名字来定位Action, 所以我把相应Action都赋上了Name属性.

在ASP.NET Core 2.0里面使用IUrlHelper需要在Startup里面注册:

           services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

            services.AddScoped<IUrlHelper>(factory =>

            {

                var actionContext = factory.GetService<IActionContextAccessor>()

                                        .ActionContext;

                return new UrlHelper(actionContext);

            });

最后, 在调用Get和Post方法返回的时候使用CreateLinksForVehicle方法对要返回的VehicleViewModel进行包装, 生成links.

下面我们可以使用POSTMAN来测试一下效果:

首先添加一笔数据:

返回结果:

没问题, 这就是我想要的效果.

然后看一下GET:

也没问题.

针对集合类返回结果

上面的例子都是返回单笔数据, 如果返回集合类的数据, 我当然可以遍历集合里的每一个数据, 然后做CreateLinksForVehicle. 但是这样就无法添加这个GET集合Action本身的link了. 所以针对集合类结果需要再做一个父类.

LinkedCollectionResourceWrapperViewModel.cs:

using System.Collections.Generic;


namespace SalesApi.Core.Abstractions.Hateoas

{

    public class LinkedCollectionResourceWrapperViewModel<T> : LinkedResourceBaseViewModel

        where T : LinkedResourceBaseViewModel

    {

        public LinkedCollectionResourceWrapperViewModel(IEnumerable<T> value)

        {

            Value = value;

        }


        public IEnumerable<T> Value { get; set; }

    }

}

这里, 我把集合数据包装到了这个类的value属性里.

然后在Controller里面添加另外一个方法:

private LinkedCollectionResourceWrapperViewModel<VehicleViewModel> CreateLinksForVehicle(LinkedCollectionResourceWrapperViewModel<VehicleViewModel> vehiclesWrapper)

        {

            vehiclesWrapper.Links.Add(

                new LinkViewModel(_urlHelper.Link("GetAllVehicles", new { }),

                "self",

                "GET"

            ));


            return vehiclesWrapper;

        }

然后针对集合查询的ACTION我这样修改:

        [HttpGet(Name = "GetAllVehicles")]

        public async Task<IActionResult> GetAll()

        {

            var items = await _vehicleRepository.All.ToListAsync();

            var results = Mapper.Map<IEnumerable<VehicleViewModel>>(items);

            results = results.Select(CreateLinksForVehicle);

            var wrapper = new LinkedCollectionResourceWrapperViewModel<VehicleViewModel>(results);

            return Ok(CreateLinksForVehicle(wrapper));

        }

这里主要有三项工作:

  1. 通过results.Select(x => CreateLinksForVehicle(x)) 对集合的每个元素添加links.

  2. 然后把集合用上面刚刚建立的父类进行包装

  3. 使用刚刚建立的CrateLinksForVehicle重载方法对这个包装的集合添加本身的link.

最后看看效果:

嗯, 没问题.  

这是第一种实现HATEOAS的方案, 另外一种等我稍微研究下再写.

原文:https://www.cnblogs.com/cgzl/p/8726805.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321737.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【招聘(北京)】.NETCORE开发工程师(微服务方向)

组织&#xff1a;华汽集团北京研发中心位置&#xff1a;北京市朝阳区焦奥中心官网&#xff1a;www.sinoauto.com邮箱&#xff1a;taoxu.weisinoauto.com 项目&#xff1a;打造面向国内汽车后市场用户的一站式云服务平台&#xff08;华汽云&#xff09;&#xff0c;形态包括B2B、…

确保线程安全下使用Queue的Enqueue和Dequeue

场景是这样&#xff0c;假设有一台设备会触发类型为Alarm的告警信号&#xff0c;并把信号添加到一个Queue结构中&#xff0c;每隔一段时间这个Queue会被遍历检查&#xff0c;其中的每个Alarm都会调用一个相应的处理方法。问题在于&#xff0c;检查机制是基于多线程的&#xff0…

编写一个Java程序,其中包含三个线程: 厨师(Chef)、服务员(Waiter)和顾客(Customer)

编写一个Java程序&#xff0c;其中包含三个线程: 厨师(Chef)、服务员(Waiter)和顾客(Customer)。他们的行动如下: 厨师准备菜肴&#xff0c;每次准备一个。服务员等待菜肴准备好&#xff0c;然后将其送到顾客那里。顾客等待服务员送来菜看后才开始吃。所有三个角色应该循环进行…

Hangfire使用ApplicationInsigts监控

起因我司目前使用清真的ApplicationInsights来做程序级监控。&#xff08;ApplicationInsights相关文档: https://azure.microsoft.com/zh-cn/services/application-insights/ &#xff09;其实一切都蛮好的&#xff0c;但是我们基于Hangfire的Job系统却无法被Ai所监控到&#…

NET主流ORM框架分析

接上文我们测试了各个ORM框架的性能&#xff0c;大家可以很直观的看到各个ORM框架与原生的ADO.NET在境删改查的性能差异。这里和大家分享下我对ORM框架的理解及一些使用经验。ORM框架工作原理所有的ORM框架的工作原理都离不开下面这张图&#xff0c;只是每个框架的实现程度不同…

20、java中的类加载机制

1、类加载机制是什么&#xff1f; 类加载机制指的就是jvm将类的信息动态添加到内存并使用的一种机制。 2、那么类加载的具体流程是什么呢&#xff1f; 一般说类加载只有三步&#xff1a;加载、连接和初始化&#xff0c;其中连接包括验证、准备和解析&#xff0c;用于将运行时加…

【北京】BXUG第12期活动基于 .NET Core构建微服务和Xamarin

分享主题&#xff1a;基于 .NET Core构建微服务实战分享分享者&#xff1a;薛锋 北京切尔思科技架构师 兼任东北大学信息安全工程师和技术主播&#xff0c;行业内专注于研究 .NET Core和Web应用&#xff0c;具有比较扎实的技术基础和数年的从业经历。在GitHub上主持数个开…

谈谈ASP.NET Core中的ResponseCaching

前言前面的博客谈的大多数都是针对数据的缓存&#xff0c;今天我们来换换口味。来谈谈在ASP.NET Core中的ResponseCaching&#xff0c;与ResponseCaching关联密切的也就是常说的HTTP缓存。在阅读本文内容之前&#xff0c;默认各位有HTTP缓存相关的基础&#xff0c;主要是Cache-…

使用 dynamic 类型让 ASP.NET Core 实现 HATEOAS 结构的 RESTful API

上一篇写的是使用静态基类方法的实现步骤: 使用dynamic (ExpandoObject)的好处就是可以动态组建返回类型, 之前使用的是ViewModel, 如果想返回结果的话, 肯定需要把ViewModel所有的属性都返回, 如果属性比较多, 就有可能造成性能和灵活性等问题. 而使用ExpandoObject(dynamic)就…

使用 BenchmarkDotnet 测试代码性能

先来点题外话&#xff0c;清明节前把工作辞了&#xff08;去 tm 的垃圾团队&#xff0c;各种拉帮结派、勾心斗角&#xff09;。这次找工作就得慢慢找了&#xff0c;不能急了&#xff0c;希望能找到个好团队&#xff0c;好岗位吧。顺便这段时间也算是比较闲&#xff0c;也能学习…

2017西安交大ACM小学期数论 [阅兵式]

阅兵式 发布时间: 2017年6月25日 12:53 最后更新: 2017年7月3日 09:27 时间限制: 1000ms 内存限制: 128M 描述 阅兵式上&#xff0c;将士们排成一个整齐的方阵&#xff0c;每个将士面朝前方。问正中心的将士能向前看到几个将士&#xff1f;注意&#xff0c;一条直线上的将…

28、jdbc操作数据库(5)

介绍一个稍微封装了jdbc的工具类org.apache.commons.dbutils&#xff0c;使用dbutils可以简化对数据库操作程序的开发。 API介绍 接下来通过实例的方式说一下dbutils的具体使用 添加jar包&#xff1a;commons-dbutils-1.7.jar 增、删、改 进行增、删、改操作&#xff0c;在…

2017西安交大ACM小学期数论 [等差数列]

等差数列 发布时间: 2017年6月25日 13:42 最后更新: 2017年7月3日 09:27 时间限制: 1000ms 内存限制: 128M 描述 给定正整数n&#xff0c;试问存在多少个和为n的等差数列&#xff1f; 当然&#xff0c;等差数列中每一项要为非负整数&#xff0c;且不考虑降序的等差数列。…

上古时期(大雾)的数据结构pdf

分块点分治Treap byWYCby\ WYCby WYC Part1 分块 概念 就是将nnn个数分成若干个块&#xff0c;然后要处理的时候整块一起的加上局部的直接暴力。 如果将块的大小分配好一般每次都是O(n)O(\sqrt n)O(n​)的。 而且因为十分暴力&#xff0c;所以有很多优秀的性质。 实现方法 …

33、JAVA_WEB开发基础之会话机制

会话是什么 一个客户端浏览器与web服务器之间连续发生的一系列请求和响应过程就是会话&#xff0c;这些过程中产生的一系列信息就是会话信息&#xff0c;会话机制就是用于维护这些信息一致性的一种技术。通俗的说就是&#xff0c;一个A账号访问服务器&#xff0c;进行多次交互…

35、JAVA_WEB开发基础之过滤器

是什么 过滤器javaweb的一个重要组件&#xff0c;一种规范&#xff0c;可以对发送到serlvet的请求进行拦截和响应进行过滤。实际开发中可以使用过滤器来对访问服务器的请求进行过滤&#xff0c;以提高安全性 过滤器的原理 可以配置过滤器对指定的请求进行过滤&#xff0c;就…

2、安装和连接mysql

安装mysql 1、官网下载mysql 下载网址&#xff1a;https://www.mysql.com/ 2、解压并配置mysql 解压下载的&#xff08;前提下载的zip版本的mysql&#xff09;mysql安装包&#xff0c;放到指定磁盘 配置环境变量&#xff1a;将mysql下的bin目录的全路径名配置到环境变量的p…

6、mysql中字段

对数据表的操作是比较重要的&#xff0c;在实际开发中&#xff0c;日常做的主要工作就是对数据表的操作 对数据表的操作分为两大部分&#xff1a;操作数据表的结构、操作数据表中的数据 组成数据表的基本单元就是字段&#xff0c;所以&#xff0c;接下来先介绍一下mysql中的字…

在Linux环境下使用Apache部署ASP.NET Core

在前几篇文章中我们一起探讨了如何在Linux环境中安装ASP.NET Core运行时环境及将ASP.NET Core项目部署在Jexus中&#xff0c;这篇文章中我们将探讨如何将ASP.NET Core部署于Apache&#xff08;阿帕奇&#xff09;中。 很幸运能够和大家一起学习和探讨ASP.NET Core本文章运行…

傲娇码农的自我修养

一个热爱自己职业的人一定会对自己的工作充满自豪感&#xff0c;同样&#xff0c;也应该对自己的工作充满热情和自信。对自己的专业能力骄傲而不自满。身为一个码农&#xff0c;如果你热爱自己的工作&#xff0c;我想&#xff0c;你很有可能也是一位傲娇码农。在我的眼里&#…