数据模型:DDD领域驱动模型 -->标记 后续进行学习
比较流行的数据持久化模式:repository仓储模式
Models文件夹存放与数据有关的代码
注:
1.无论是put patch get post ,都不能直接接触到模型数据,必须创建响应的Dto模型;
2.当我们进行Put更新的时候,要记得使用AutoMapper,它可以直接将Dto墨香映射到Entity实例对象,而我们只需要Save那么一小下就ok啦
3.Dto类的做好了AutoMapper映射,Dto中的子数据也要进行AutoMapper否则会报错
前期开发流程:
1.在Models文件夹下首先创建业务数据模型(旅游网站:基本都是围绕着旅游路线的,所以首先创建旅游路线)2.在Database文件夹内创建数据库映射配置文件=>执行数据库迁移(在数据库中添加Json种子数据)
3.在Services文件夹内创建ITouristRouteRepository仓库接口服务,并创建实现该接口的类
4.在Controllers文件夹下创建控制器
5.正式的开始
(1)Status Code 返回码存在的问题
当我们根据touristRouteId进行查询指定的路线图的时候,如果输入了错误的touristRouteId,服务端没有查询到数据,返回的代码为 204 not content(执行成功没有内容需要显示)
这里是不对的,服务器应该返回的Status Code 为 404 not found ;
下面的代码解决了这个问题:(我们判断从Repository中拿到的对象是否为空,来确定返回的Status Code)
[HttpGet]//http://localhost:5000/api/TouristRoutes
public IActionResult GetTouristRoutes()
{
var touristRoutesFromRepo = _touristRouteRepository.GetTouristRoutes();
if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
{
return NotFound("没有旅游路线图");
}
return Ok(touristRoutesFromRepo);
}
[HttpGet]//http://localhost:5000/api/TouristRoutes/99BA5433-DF5F-A898-C8E0-78B8BA55F251
[Route("{touristRouteId:Guid}")]//(:Guid确保传进来的数据为Guid)
public IActionResult GetTouristRouteById(Guid touristRouteId)
{
var touristRouteFromRepo = _touristRouteRepository.GetTouristRoute(touristRouteId);
if (touristRouteFromRepo == null)
{
return NotFound($"没有查询到该路由路线图,请检查该Id:{touristRouteId}的路线图是否存在,或者联系管理员");
}
return Ok(touristRouteFromRepo);
}
(2)内容协商(Content Negotiation)与数据格式
前后端分离的开发模式,需要Api对应不同的前端,返回不同的数据格式(json,xml)
在这里需要实现的是:Api可以根据请求的数据类型,动态的响应不同类型的数据格式
请求头部的媒体类型定义"accept"与"Content-type" ,遇见无法识别的格式返回错误代码 406 unacceptable
ASP.NET Core 支持内容协商,自动化处理
在这里我们发现在我们请求application/xml格式的数据的时候,服务端返回的依然是json的格式,这个时候正确的响应应该是 406 unacceptable ,下面我们处理这个问题
并且添加了对Xml格式数据的支持
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(setupAction =>
{
setupAction.ReturnHttpNotAcceptable = true;//这里配置了对不支持的数据格式的请求返回406 unacceptable
}
).AddXmlDataContractSerializerFormatters();//添加了对Xml格式数据的支持
}
(3)Model数据模型与Dto(Data Transfer Object)数据传输对象
直接向前端发挥数据模型,会暴露系统的业务核心(使用Dto可以屏蔽我们不希望暴露的核心业务)
颗粒度太粗,无法对输出的数据做精细的调整(eg:数据模型有一个,而要展示给不同客户不同的数据,这个时候就需要Dto了)
Model面向业务
Dto面向界面ui
下面我们实现一下:Model与Dto分离
首先我们创建Dtos文件夹,在文件夹内创建TouristRouteDto
然后使用Automapper进行映射 nuget:AutoMapper.Extensions.Microsoft.DependencyInjection
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
然后创建Profile文件夹,在文件夹内创建继承Profile的类
AutoMapper在完成依赖注入以后,回去寻找项目文件夹中命名为Profile的文件夹,扫描文件夹中的所有文件,在Profile的构造函数中完成对对象映射的配置
public class TouristRouteProfile:Profile
{
public TouristRouteProfile()
{
CreateMap<TouristRoute, TouristRouteDto>()
.ForMember(dest => dest.Price,
opt => opt.MapFrom(src => src.OriginalPrice * (decimal)(src.DiscountPresent ?? 1))
)
.ForMember(dest => dest.TravelDays,
opt => opt.MapFrom(src => src.TravelDays.ToString())
)
.ForMember(dest => dest.TravelDays,
opt => opt.MapFrom(src => src.TripType.ToString())
)
.ForMember(dest => dest.DepartureCity,
opt => opt.MapFrom(src => src.DepartureCity.ToString())
);
}
}
然后我们在控制器中注入服务
执行映射
var touristRouteDto = _mapper.Map<TouristRouteDto>(touristRouteFromRepo);
调用-完成!
(4)获取嵌套对象关系型数据
eg:通过路由路线获取路由路线图片
这里需要在IRepository中添加新的功能
需求:当我们点击一个旅游路线的时候,接下来要显示这个旅游路线所有的路线图片,而路线和路线图就是嵌套的关系
下面是代码时间,我们仅仅添加了一个include方法,这个方法使得两个表连接到了一起
我们在仓库中将拿到的子数据一同返回到controller中,因为我们使用AutoMapper,它会自动将TouristRoute和TourstRouteDto中属性名称相同的对象进行映射,当然也包括集合类
所以在后面返回的数据中就包含了,子集合中的数据
public IEnumerable<TouristRoute> GetTouristRoutes()
{
//Include就是efcore中连接两张表的方法
return _context.TouristRoutes.Include(t => t.TouristRoutePictures);
}
(4)HttpHead请求
只需要在Action前添加[httphead],它返回的信息没有主体
This method is often used for testing hypertext links for validity, accessibility, and recent modification.
(4)向Api传递参数
这里有多种方法:
1.使用Attribute
[FromQuery] 从Url的参数字符串中获取(地址栏)
[FromBody] 主题数据中获取
[FromForm] 表单数据中获取
[FromRoute] Mvc架构下的Route路由URL的参数
[FromService] 数据来源于已注入的服务依赖
重点区分FromQuery和FromRoute
FromQuery:http://localhost:5000/api/TouristRoutes/?Id=99ba5433-df5f-a898-c8e0-78b8ba55f251
FromRoute:http://localhost:5000/api/TouristRoutes/99ba5433-df5f-a898-c8e0-78b8ba55f251
-----------------------------华丽的分割线---------------------------------------------------------------
从这里开始,会有记录风格一些新的变化(面向业务需求开发,而不是面向开发而开发)
下面我们对代码进行了业务上的优化
客户在网页上需要根据一些条件进行检索过滤,快速的得到用户想要的信息
下面我们异步的实现了过滤,为了进一步提高性能,我们还使用了IQueryable接口类型(延迟查询)
只有在使用了ToList()、FirstOrDefault(),这种聚合查询的时候,EfCore才会执行数据库查询
在这之前,我们可以默认IQueryable只是存储了查询的语句
代码简介:
首先我们从Url中的QueryString参数拿到过滤的条件和值,
因为评分过滤条件包含了(大于/小于/等于)+评分值,所以我们需要使用到正则表达式将其从一个字符串中分离为2个,在字符串未拆分前,我们需要声明存储分离后的值的变量,在这里有一个为int值类型的ratingValue评分值,不能为空,且在后面的代码中我们判定ratingValue值有效的条件为>=0,所以我们设置其默认值为-1即可;
[HttpGet]//http://localhost:5000/api/TouristRoutes?keyword=埃及&ratingValue=largerThan4
[HttpHead]
public async Task<IActionResult> GetTouristRoutes(
[FromQuery]string keyword,
[FromQuery(Name = "ratingValue")] string rating //lessThan,largerThan,equalTo ====>lessThan3,largerThan4,equalTo1
)
{
Regex regex = new Regex(@"([A-Za-z0-9-]+)(d+)");
string operatorType = string.Empty;
int ratingValue = -1;
////确保rating不为空,否则会报错
if (!string.IsNullOrEmpty(rating))
{
Match match = regex.Match(rating.Trim());
if (match.Success)
{
operatorType = match.Groups[1].Value;
ratingValue = int.Parse(match.Groups[2].Value);
}
}
var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutes(keyword, operatorType, ratingValue);
if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
{
return NotFound("您所查找的旅游路线图不存在,如有问题请拨打客服电话:110");
}
var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
return Ok(touristRoutesDto);
}
仓库代码:
public async Task<IEnumerable<TouristRoute>> GetTouristRoutes(string keyword, string operatorType, int ratingValue)
{
IQueryable<TouristRoute> Queryable = _context.TouristRoutes.Include(x => x.TouristRoutePictures);
//Include就是efcore中连接两张表的方法
if (!String.IsNullOrWhiteSpace(keyword))
{
Queryable = Queryable.Where(x => x.Tittle.Contains(keyword));
}
//ratingVlaue>=0的时候才需要过滤
if (ratingValue >= 0 && operatorType != null)
{
switch (operatorType)
{
case "lessThan": Queryable = Queryable.Where(x => x.Rating <= ratingValue);break;
case "largerThan": Queryable = Queryable.Where(x => x.Rating >= ratingValue);break;
case "equalThan": Queryable = Queryable.Where(x => x.Rating == ratingValue);break;
default:
break;
}
return await Queryable.ToListAsync();
}
return await Queryable.ToListAsync();
}
下面我们再对上面的程序进行优化:封装资源过滤器
通过创建封装资源过滤参数的[TouristRouteResourceParamaters.cs]类(实现过滤参数与控制器的解耦合)
同时将正则表达式一部分代码,转移到了这个类下的_rating变量的Rating的属性中
[HttpGet]//http://localhost:5000/api/TouristRoutes?keyword=埃及&ratingValue=largerThan4
[HttpHead]
public async Task<IActionResult> GetTouristRoutes(
[FromQuery]TouristRouteResourceParamaters paramaters //lessThan,largerThan,equalTo ====>lessThan3,largerThan4,equalTo1
)
{
var touristRoutesFromRepo = await _touristRouteRepository.GetTouristRoutes(paramaters.Keyword, paramaters.RatingOperatorType, paramaters.RatingValue);
if (touristRoutesFromRepo == null || touristRoutesFromRepo.Count() <= 0)
{
return NotFound("您所查找的旅游路线图不存在,如有问题请拨打客服电话:110");
}
var touristRoutesDto = _mapper.Map<IEnumerable<TouristRouteDto>>(touristRoutesFromRepo);
return Ok(touristRoutesDto);
}
public class TouristRouteResourceParamaters
{
public string Keyword { get; set; }
public string RatingOperatorType { get; set; }
public int RatingValue { get; set; } = -1;
private string _rating;
public string Rating
{
get
{
return _rating;
}
set
{
if (value != null)
{
Regex regex = new Regex(@"([A-Za-z0-9-]+)(d+)");
Match match = regex.Match(value);
if (match.Success)
{
RatingOperatorType = match.Groups[1].Value;
RatingValue = int.Parse(match.Groups[2].Value);
}
}
_rating = value;
}
}//lessThan,largerThan,equalTo ====>lessThan3,largerThan4,equalTo1
}
我们Post资源偶,还需创建子资源
[HttpPost]
public async Task<IActionResult> CreateTouristRoutePictures(
[FromRoute] Guid touristRouteId,
[FromBody] TouristRouteForCreationPicDto touristRouteForCreationPicDto
)
{
if (!_touristRouteRepository.TouristRouteExists(touristRouteId))
{
return NotFound();
}
var pictureModel = _mapper.Map<TouristRoutePicture>(touristRouteForCreationPicDto);
_touristRouteRepository.AddTouristRoutePicture(touristRouteId, pictureModel);
await _touristRouteRepository.Save();
var pictureDto = _mapper.Map<TouristRoutePictureDto>(pictureModel);
return CreatedAtAction(nameof(GetPicture), new { touristRouteId = touristRouteId, picId = pictureModel.Id },pictureDto);
}
仓库代码
public void AddTouristRoutePicture(Guid TouristRouteId, TouristRoutePicture touristRoutePicture)
{
if (TouristRouteId == Guid.Empty)
{
throw new ArgumentNullException(nameof(TouristRouteId));
}
if (touristRoutePicture==null)
{
throw new ArgumentNullException(nameof(touristRoutePicture));
}
touristRoutePicture.TouristRouteId = TouristRouteId;
_context.TouristRoutePictures.Add(touristRoutePicture);
}
当然我们页可以在Post提交TouristRoute路由路线的时候,在json中添加子数据直接提交
其他的映射工作AutoMapper已经替我们完成了,所以映射框架在工作中是能大幅度提高工作效率的
数据验证:
目前为止,我们提交的数据中如果Tittle=null,那么就会返回数据库错误,无法提交。因为过多的错误可能导致数据库崩溃
所以我们选择在Dto层面上进行数据验证,这样就避免了这一个问题
这个验证和模型中的验证属性标签是一样的
[Required(ErrorMessage ="Tittle不可以为空值")]
当然我们还可以添加自定义验证
就是使Dto类实现IValidatableObject接口,在Validate方法中实现数据的验证
自定义错误信息和错误报告.ConfigureApiBehaviorOptions
services.AddControllers(
configure: setup =>
{
setup.ReturnHttpNotAcceptable = true;
// setup.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
}
)
.AddXmlDataContractSerializerFormatters()
.ConfigureApiBehaviorOptions(
setup => setup.InvalidModelStateResponseFactory = context =>
{
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Type = "旅游网",
Title = "error",
Status = StatusCodes.Status422UnprocessableEntity,
Detail = "请看详细信息",
Instance = context.HttpContext.Request.Path
};
problemDetails.Extensions.Add("traceId", context.HttpContext.TraceIdentifier);
return new UnprocessableEntityObjectResult(problemDetails)
{
ContentTypes = { "application/problem+json" }
};
}
);
添加类级别的验证;在使用的类上面添加[TouristRouteTitleMustBeDifferentFromDescriptionAttribute]标签即可,后面我们会实现解耦
/// <summary>
/// 类级别的数据验证
/// </summary>
public class TouristRouteTitleMustBeDifferentFromDescriptionAttribute:ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var touristRouteDto =(TouristRouteForCreationgDto) validationContext.ObjectInstance;
if (touristRouteDto.Tittle == touristRouteDto.Description)
{
return new ValidationResult("路线名称必须与路线描述不同", new[] { "TouristRouteForCreationDto" });
}
return ValidationResult.Success;
}
}
将Json中的值映射到实体类中的属性:
Services.Configure<映射到的类>(_configuration.GetSectiong(key:"键值"));
PUT方法
这里有一点特别,可能是因为太过于简洁,因为AutoMapper已经为我们完成了
[HttpPut]
[Route("{touristRouteId:Guid}")]
public async Task<IActionResult> UpdateTouristRouet(
[FromRoute] Guid TouristRouteId,
[FromBody] TouristRouteForUpdateDto touristRouteForUpdateDto)
{
if(!_touristRouteRepository.TouristRouteExists(TouristRouteId))
{
return NotFound("没有找到旅游路线");
}
var touristRoute = _touristRouteRepository.GetTouristRoute(TouristRouteId);
//在这里Map()会直接将传进来的对象映射到TouristRoute的Entity instance上,我们只需要.save()提交一下即可;
var touristRoutePut = _mapper.Map(touristRouteForUpdateDto, touristRoute);
await _touristRouteRepository.Save();
return NoContent();
}
JSON Patch 6个操作
add
move
remove
copy
replace
test
[
{"op":"replace","path":"/title","value":"福岛3日游" }
]
这里需要使用到jsonPatch框架和NewtonsoftJson框架