使用两种方法让 ASP.NET Core 实现遵循 HATEOAS 结构的 RESTful 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.

最后看看效果:

嗯, 没问题. 

第一种方案 -- 使用dynamic类型

使用dynamic (ExpandoObject)的好处就是可以动态组建返回类型, 之前使用的是ViewModel, 如果想返回结果的话, 肯定需要把ViewModel所有的属性都返回, 如果属性比较多, 就有可能造成性能和灵活性等问题. 而使用ExpandoObject(dynamic)就可以解决这个问题.

返回一个对象

返回一个dynamic类型的对象, 需要把所需要的属性从ViewModel抽取出来并转化成dynamic对象, 这里所需要的属性通常是从参数传进来的, 例如针对下面的CustomerViewModel类, 参数可能是这样的: "Name, Company":

using System;
using SalesApi.Core.Abstractions.DomainModels;

namespace SalesApi.ViewModels
{
    public class CustomerViewModel: EntityBase
    {
        public string Company { get; set; }
        public string Name { get; set; }
        public DateTimeOffset EstablishmentTime { get; set; }
    }
}

还需要一个Extension Method可以把对象按照需要的属性转化成dynamic类型:

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SalesApi.Shared.Helpers
{
    public static class ObjectExtensions
    {
        public static ExpandoObject ToDynamic<TSource>(this TSource source, string fields = null)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var dataShapedObject = new ExpandoObject();
            if (string.IsNullOrWhiteSpace(fields))
            {
                // 所有的 public properties 应该包含在ExpandoObject里 
                var propertyInfos = typeof(TSource).GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                foreach (var propertyInfo in propertyInfos)
                {
                    // 取得源对象上该property的值
                    var propertyValue = propertyInfo.GetValue(source);
                    // 为ExpandoObject添加field
                    ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
                }
                return dataShapedObject;
            }

            // field是使用 "," 分割的, 这里是进行分割动作.
            var fieldsAfterSplit = fields.Split(',');
            foreach (var field in fieldsAfterSplit)
            {
                var propertyName = field.Trim();

                // 使用反射来获取源对象上的property
                // 需要包括public和实例属性, 并忽略大小写.
                var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                if (propertyInfo == null)
                {
                    throw new Exception($"没有在‘{typeof(TSource)}’上找到‘{propertyName}’这个Property");
                }

                // 取得源对象property的值
                var propertyValue = propertyInfo.GetValue(source);
                // 为ExpandoObject添加field
                ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
            }

            return dataShapedObject;
        }
    }
}

注意: 这里的逻辑是如果没有选择需要的属性的话, 那么就返回所有合适的属性.

然后在CustomerController里面:

首先创建为对象添加link的方法:

        private IEnumerable<LinkViewModel> CreateLinksForCustomer(int id, string fields = null)
        {
            var links = new List<LinkViewModel>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                links.Add(
                    new LinkViewModel(_urlHelper.Link("GetCustomer", new { id = id }),
                    "self",
                    "GET"));
            }
            else
            {
                links.Add(
                    new LinkViewModel(_urlHelper.Link("GetCustomer", new { id = id, fields = fields }),
                    "self",
                    "GET"));
            }

            links.Add(
                new LinkViewModel(_urlHelper.Link("DeleteCustomer", new { id = id }),
                "delete_customer",
                "DELETE"));

            links.Add(
                new LinkViewModel(_urlHelper.Link("CreateCustomer", new { id = id }),
                "create_customer",
                "POST"));

            return links;
        }

针对返回一个对象, 添加了本身的连接, 添加的连接 以及 删除的连接.

然后修改Get和Post的Action:

        [HttpGet]
        [Route("{id}", Name = "GetCustomer")]
        public async Task<IActionResult> Get(int id, string fields)
        {
            var item = await _customerRepository.GetSingleAsync(id);
            if (item == null)
            {
                return NotFound();
            }
            var customerVm = Mapper.Map<CustomerViewModel>(item);
            var links = CreateLinksForCustomer(id, fields);
            var dynamicObject = customerVm.ToDynamic(fields) as IDictionary<string, object>;
            dynamicObject.Add("links", links);
            return Ok(dynamicObject);
        }

        [HttpPost(Name = "CreateCustomer")]
        public async Task<IActionResult> Post([FromBody] CustomerViewModel customerVm)
        {
            if (customerVm == null)
            {
                return BadRequest();
            }

            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            var newItem = Mapper.Map<Customer>(customerVm);
            _customerRepository.Add(newItem);
            if (!await UnitOfWork.SaveAsync())
            {
                return StatusCode(500, "保存时出错");
            }

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

            var links = CreateLinksForCustomer(vm.Id);
            var dynamicObject = vm.ToDynamic() as IDictionary<string, object>;
            dynamicObject.Add("links", links);

            return CreatedAtRoute("GetCustomer", new { id = dynamicObject["Id"] }, dynamicObject);
        }

红色部分是相关的代码. 创建links之后把vm对象按照需要的属性转化成dynamic对象. 然后往这个dynamic对象里面添加links属性. 最后返回该对象.

下面测试一下.

POST:

结果:

由于POST方法里面没有选择任何fields, 所以返回所有的属性.

下面试一下GET:

再试一下GET, 选择几个fields:

OK, 效果都如预期.

但是有一个问题, 因为返回的json的Pascal case的(只有dynamic对象返回的是Pascal case, 其他ViewModel现在返回的都是camel case的), 而camel case才是更好的选择 .

所以在Startup里面可以这样设置:

            services.AddMvc(options =>
            {
                options.ReturnHttpNotAcceptable = true;
                // the default formatter is the first one in the list.
                options.OutputFormatters.Remove(new XmlDataContractSerializerOutputFormatter());

                // set authorization on all controllers or routes
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()
                    .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
            })
            .AddJsonOptions(options =>
            {
                options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            })
            .AddFluetValidations();

然后再试试:

OK.

返回集合

 首先编写创建links的方法:

        private IEnumerable<LinkViewModel> CreateLinksForCustomers(string fields = null)
        {
            var links = new List<LinkViewModel>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                links.Add(
                   new LinkViewModel(_urlHelper.Link("GetAllCustomers", new { fields = fields }),
                   "self",
                   "GET"));
            }
            else
            {
                links.Add(
                   new LinkViewModel(_urlHelper.Link("GetAllCustomers", new { }),
                   "self",
                   "GET"));
            }
            return links;
        }

这个很简单.

然后需要针对IEnumerable<T>类型创建把ViewModel转化成dynamic对象的Extension方法:

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Reflection;

namespace SalesApi.Shared.Helpers
{
    public static class IEnumerableExtensions
    {
        public static IEnumerable<ExpandoObject> ToDynamicIEnumerable<TSource>(this IEnumerable<TSource> source, string fields)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var expandoObjectList = new List<ExpandoObject>();
            var propertyInfoList = new List<PropertyInfo>();
            if (string.IsNullOrWhiteSpace(fields))
            {
                var propertyInfos = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
                propertyInfoList.AddRange(propertyInfos);
            }
            else
            {
                var fieldsAfterSplit = fields.Split(',');
                foreach (var field in fieldsAfterSplit)
                {
                    var propertyName = field.Trim();
                    var propertyInfo = typeof(TSource).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                    if (propertyInfo == null)
                    {
                        throw new Exception($"Property {propertyName} wasn't found on {typeof(TSource)}");
                    }
                    propertyInfoList.Add(propertyInfo);
                }
            }

            foreach (TSource sourceObject in source)
            {
                var dataShapedObject = new ExpandoObject();
                foreach (var propertyInfo in propertyInfoList)
                {
                    var propertyValue = propertyInfo.GetValue(sourceObject);
                    ((IDictionary<string, object>)dataShapedObject).Add(propertyInfo.Name, propertyValue);
                }
                expandoObjectList.Add(dataShapedObject);
            }

            return expandoObjectList;
        }
    }
}

注意: 反射的开销很大, 注意性能.

然后修改GetAll方法:

        [HttpGet(Name = "GetAllCustomers")]
        public async Task<IActionResult> GetAll(string fields)
        {
            var items = await _customerRepository.GetAllAsync();
            var results = Mapper.Map<IEnumerable<CustomerViewModel>>(items);
            var dynamicList = results.ToDynamicIEnumerable(fields);
            var links = CreateLinksForCustomers(fields);
            var dynamicListWithLinks = dynamicList.Select(customer =>
            {
                var customerDictionary = customer as IDictionary<string, object>;
                var customerLinks = CreateLinksForCustomer(
                    (int)customerDictionary["Id"], fields);
                customerDictionary.Add("links", customerLinks);
                return customerDictionary;
            });
            var resultWithLink = new {
                Value = dynamicListWithLinks,
                Links = links
            };
            return Ok(resultWithLink);
        }

红色部分是相关代码.

测试一下:

不选择属性:

选择部分属性:

OK.

HATEOAS这部分就写到这.

其实 翻页的逻辑很适合使用HATEOAS结构. 有空我再写一个翻页的吧.

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏草根专栏

用VSCode开发一个基于asp.net core 2.0/sql server linux(docker)/ng5/bs4的项目(3)

由于本文主要是讲VSCode开发等, 所以相关等一些angular/.net core的知识就相对少讲点. 我把需求改一下, 如图: ? 由于efcore目前还...

39290
来自专栏Java成神之路

Java钉钉开发_02_免登授权(身份验证)

将所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式 (即 key1=value1&key2=value2…)拼接成字符串...

42120
来自专栏c#开发者

Winform 的一个多线程绑定DataGrid数据源的例子

我们都知道简单的运用多线程的方法有 1/ Thread thread=new Thread(new StartThread(this.method))     ...

31590
来自专栏技术之路

Caliburn.Micro学习笔记(三)----事件聚合IEventAggregator和 Ihandle<T>

今天 说一下Caliburn.Micro的IEventAggregator和IHandle<T>分成两篇去讲这一篇写一个简单的例子 看一它的的实现和源码 下一篇...

29990
来自专栏大内老A

Enterprise Library Policy Injection Application Block 之三:PIAB的扩展—创建自定义CallHandler(提供Source Code下载)

本系列的第一部分对PIAB使用场景进行了简单的介绍,作中阐述了通过PI(Policy Injection)的方式实现了Business Logic和Non-Bu...

351100
来自专栏walterlv - 吕毅的博客

使用 C# 代码创建快捷方式文件

发布于 2015-04-07 04:48 更新于 2018-08...

25410
来自专栏GIS讲堂

一个GISER 6.7的祝福

一年一度的高考今天开始了,回想10年前,那是我第一次高考;10年后,作为一个GISER,在此给大家献上一个GISER的祝福,祝愿各位考生:考神附体,考完报考GI...

15340
来自专栏大内老A

我的WCF之旅(7):面向服务架构(SOA)和面向对象编程(OOP)的结合——如何实现Service Contract的继承

当今的IT领域,SOA已经成为了一个非常时髦的词,对SOA风靡的程度已经让很多人对SOA,对面向服务产生误解。其中很大一部分人甚至认为面向服务将是面向对象的终结...

19650
来自专栏Java成神之路

Java微信公众平台开发_06_素材管理

30820
来自专栏blackheart的专栏

[C#6] 8-异常增强

0. 目录 C#6 新增特性目录 1. 在catch和finally块中使用await 在C#5中引入一对关键字await/async,用来支持新的异步编程模型...

20650

扫码关注云+社区

领取腾讯云代金券