主頁 > .NET開發 > Asp.NetCoreWebApi - RESTful Api

Asp.NetCoreWebApi - RESTful Api

2020-09-22 16:17:18 .NET開發

參考文章

  • ASP.NET Core Web API 開發-RESTful API實作
  • 理解HTTP冪等性
  • 某站微軟Mvp楊旭的Asp.NetCore WebApi的視頻

REST概述

REST : 具象狀態傳輸(Representational State Transfer,簡稱REST),是Roy Thomas Fielding博士于2000年在他的博士論文 "Architectural Styles and the Design of Network-based Software Architectures" 中提出來的一種萬維網軟體架構風格,
目前在三種主流的Web服務實作方案中,因為REST模式與復雜的SOAPXML-RPC相比更加簡潔,越來越多的web服務開始采用REST風格設計和實作,例如,Amazon.com提供接近REST風格的Web服務執行圖書查詢;

符合REST設計風格的Web API稱為RESTful API,它從以下三個方面資源進行定義:

  • 直觀簡短的資源地址:URI,比如:http://example.com/resources/ .
  • 傳輸的資源:Web服務接受與回傳的互聯網媒體型別,比如:JSON,XML,YAML等...
  • 對資源的操作:Web服務在該資源上所支持的一系列請求方法(比如:POST,GET,PUT或DELETE).

PUT和DELETE方法是冪等方法.GET方法是安全方法(不會對服務器端有修改,因此當然也是冪等的).

ps 關于冪等方法 :
看這篇 理解HTTP冪等性.
簡單說,客戶端多次請求服務端回傳的結果都相同,那么就說這個操作是冪等的.(個人理解,詳細的看上面給的文章)

不像基于SOAP的Web服務,RESTful Web服務并沒有“正式”的標準,這是因為REST是一種架構,而SOAP只是一個協議,雖然REST不是一個標準,但大部分RESTful Web服務實作會使用HTTP、URI、JSON和XML等各種標準,

常用http動詞

括號中是相應的SQL命令.

  • GET(SELECT) : 從服務器取出資源(一項或多項).
  • POST(CREATE) : 在服務器新建一個資源.
  • PUT(UPDATE) : 在服務器更新資源(客戶端提供改變后的完整資源).
  • PATCH(UPDATE) : 在服務器更新資源(客戶端提供改變的屬性).
  • DELETE(DELETE) : 在服務器洗掉資源.

WebApi 在 Asp.NetCore 中的實作

這里以用戶增刪改查為例.

創建WebApi專案.

參考ASP.NET Core WebAPI 開發-新建WebAPI專案.

注意,本文建立的Asp.NetCore WebApi專案選擇.net core版本是2.2,不建議使用其他版本,2.1版本下會遇到依賴檔案沖突問題!所以一定要選擇2.2版本的.net core.

集成Entity Framework Core操作Mysql

安裝相關的包(為Xxxx.Infrastructure專案安裝)

  • Microsoft.EntityFrameworkCore.Design
  • Pomelo.EntityFrameworkCore.MySql

這里注意一下,Mysql官方的包是 MySql.Data.EntityFrameworkCore,但是這個包有bug,我在github上看到有人說有替代方案 - Pomelo.EntityFrameworkCore.MySql,經過嘗試,后者比前者好用.所有這里就選擇后者了.使用前者的話可能會導致資料庫遷移失敗(Update的時候).

PS: Mysql檔案原文:

Install the MySql.Data.EntityFrameworkCore NuGet package.
For EF Core 1.1 only: If you plan to scaffold a database, install the MySql.Data.EntityFrameworkCore.Design NuGet package as well.

EFCore - MySql檔案
Mysql版本要求:
Mysql版本要高于5.7
使用最新版本的Mysql Connector(2019 6/27 目前是8.x).

為Xxxx.Infrastructure專案安裝EFCore相關的包:

為Xxxx.Api專案安裝 Pomelo.EntityFrameworkCore.MySql

建立Entity和Context

ApiUser
namespace ApiStudy.Core.Entities
{
    using System;

    public class ApiUser
    {
        public Guid Guid { get; set; }
        public string Name { get; set; }
        public string Passwd { get; set; }
        public DateTime RegistrationDate { get; set; }
        public DateTime Birth { get; set; }
        public string ProfilePhotoUrl { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
    }
}
UserContext
namespace ApiStudy.Infrastructure.Database
{
    using ApiStudy.Core.Entities;
    using Microsoft.EntityFrameworkCore;

    public class UserContext:DbContext
    {
        public UserContext(DbContextOptions<UserContext> options): base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid);

            base.OnModelCreating(modelBuilder);
        }

        public DbSet<ApiUser> ApiUsers { get; set; }
    }
}

ConfigureService中注入EF服務

services.AddDbContext<UserContext>(options =>
            {
                string connString = "Server=Xxx:xxx:xxx:xxx;Database=Xxxx;Uid=root;Pwd=Xxxxx; ";
                options.UseMySQL(connString);
            });

資料庫連接字串也可以在appSettings.json中配置
image
然后通過 Configuration.GetConnectionString("mysql") 來獲得連接字串.

遷移資料庫

  • 在Tools > NuGet Package Manager > Package Manager Console輸入命令.
  • Add-Migration Xxx 添加遷移.
    PS : 如果遷移不想要,使用 Remove-Migration 命令洗掉遷移.
  • Update-Database 更新到資料庫.

遷移資料庫失敗, 提示 Unable to create an object of type '<Xxxx>Context'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

原因應該是EfCore遷移工具不知道如何創建 DbContext 導致的.

解決方案

DbContext所在的專案下新建一個類:

/// <summary>
/// 設計時DbContext的創建, 告訴EF Core遷移工具如何創建DbContext
/// </summary>
public class <Xxxx>ContextFactory : IDesignTimeDbContextFactory<<Xxxx>Context>
{
    public <Xxxx>Context CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<<Xxxx>Context>();
        optionsBuilder.UseMySql(
            @"Server=[服務器ip];Database=[資料庫]];Uid=[用戶名];Pwd=[密碼];");

        return new <Xxxx>Context(optionsBuilder.Options);
    }
}

資料庫遷移結果

為資料庫創建種子資料

  • 寫一個創建種子資料的類

    UserContextSeed
    namespace ApiStudy.Infrastructure.Database
    {
        using ApiStudy.Core.Entities;
        using Microsoft.Extensions.Logging;
        using System;
        using System.Linq;
        using System.Threading.Tasks;
    
        public class UserContextSeed
        {
            public static async Task SeedAsync(UserContext context,ILoggerFactory loggerFactory)
            {
                try
                {
                    if (!context.ApiUsers.Any())
                    {
                        context.ApiUsers.AddRange(
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "la",
                                Birth = new DateTime(1998, 11, 29),
                                RegistrationDate = new DateTime(2019, 6, 28),
                                Passwd = "123587",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "10086",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "David",
                                Birth = new DateTime(1995, 8, 29),
                                RegistrationDate = new DateTime(2019, 3, 28),
                                Passwd = "awt87495987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "1008611",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "David",
                                Birth = new DateTime(2001, 8, 19),
                                RegistrationDate = new DateTime(2019, 4, 25),
                                Passwd = "awt87495987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "1008611",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "Linus",
                                Birth = new DateTime(1999, 10, 26),
                                RegistrationDate = new DateTime(2018, 2, 8),
                                Passwd = "awt87495987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "17084759987",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "YouYou",
                                Birth = new DateTime(1992, 1, 26),
                                RegistrationDate = new DateTime(2015, 7, 8),
                                Passwd = "grwe874864987",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "17084759987",
                                Email = "[email protected]"
                            },
                            new ApiUser
                            {
                                Guid = Guid.NewGuid(),
                                Name = "小白",
                                Birth = new DateTime(1997, 9, 30),
                                RegistrationDate = new DateTime(2018, 11, 28),
                                Passwd = "gewa749864",
                                ProfilePhotoUrl = "https://www.laggage.top/",
                                PhoneNumber = "17084759987",
                                Email = "[email protected]"
                            });
    
                        await context.SaveChangesAsync();
                    }
                }
                catch(Exception ex)
                {
                    ILogger logger = loggerFactory.CreateLogger<UserContextSeed>();
                    logger.LogError(ex, "Error occurred while seeding database");
                }
            }
        }
    }
    
    
  • 修改Program.Main方法

    Program.Main
    IWebHost host = CreateWebHostBuilder(args).Build();
    
    using (IServiceScope scope = host.Services.CreateScope())
    {
        IServiceProvider provider = scope.ServiceProvider;
        UserContext userContext = provider.GetService<UserContext>();
        ILoggerFactory loggerFactory = provider.GetService<ILoggerFactory>();
        UserContextSeed.SeedAsync(userContext, loggerFactory).Wait();
    }
    
    host.Run();
    

這個時候運行程式會出現例外,打斷點看一下例外資訊:Data too long for column 'Guid' at row 1

可以猜到,Mysql的varbinary(16)放不下C# Guid.NewGuid()方法生成的Guid,所以配置一下資料庫Guid欄位型別為varchar(256)可以解決問題.

解決方案:
修改 UserContext.OnModelCreating 方法
配置一下 ApiUser.Guid 屬性到Mysql資料庫的映射:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ApiUser>().Property(p => p.Guid)
        .HasColumnType("nvarchar(256)");
    modelBuilder.Entity<ApiUser>().HasKey(u => u.Guid);
    
    base.OnModelCreating(modelBuilder);
}

支持https

將所有http請求全部映射到https

Startup中:
ConfigureServices方法注冊,并配置埠和狀態碼等:
services.AddHttpsRedirection(…)

services.AddHttpsRedirection(options =>
                {
                    options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect;
                    options.HttpsPort = 5001;
                });

Configure方法使用該中間件:

app.UseHttpsRedirection()

支持HSTS

ConfigureServices方法注冊
看官方檔案

services.AddHsts(options =>
{
    options.Preload = true;
    options.IncludeSubDomains = true;
    options.MaxAge = TimeSpan.FromDays(60);
    options.ExcludedHosts.Add("example.com");
    options.ExcludedHosts.Add("www.example.com");
});

Configure方法配置中間件管道

app.UseHsts();

注意 app.UseHsts() 方法最好放在 app.UseHttps() 方法之后.

使用SerilLog

有關日志的微軟官方檔案

SerilLog github倉庫
該github倉庫上有詳細的使用說明.

使用方法:

安裝nuget包

  • Serilog.AspNetCore
  • Serilog.Sinks.Console

添加代碼

Program.Main方法中:

Log.Logger = new LoggerConfiguration()
            .MinimumLevel.Debug()
            .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
            .Enrich.FromLogContext()
            .WriteTo.Console()
            .CreateLogger();

修改Program.CreateWebHostBuilder(...)

 public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .UseSerilog(); // <-- Add this line;
 }

自行測驗

Asp.NetCore組態檔

默認組態檔

默認 appsettings.json
ConfigurationBuilder().AddJsonFile("appsettings.json").Build()-->IConfigurationRoot(IConfiguration)

獲得配置

IConfiguration[“Key:ChildKey”]
針對”ConnectionStrings:xxx”,可以使用IConfiguration.GetConnectionString(“xxx”)

private static IConfiguration Configuration { get; set; }

public StartupDevelopment(IConfiguration config)
{
    Configuration = config;
}

...

Configuration[“Key:ChildKey”]

全域例外處理

例外處理官方檔案

下面按照我的想法實作一個全域例外處理,

namespace ApiStudy.Api.Extensions
{
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Logging;
    using System;

    public static class ExceptionHandlingExtensions
    {
        public static void UseCustomExceptionHandler(this IApplicationBuilder app,ILoggerFactory loggerFactory)
        {
            app.UseExceptionHandler(
                builder => builder.Run(async context =>
                {
                    context.Response.StatusCode = StatusCodes.Status500InternalServerError;
                    context.Response.ContentType = "text/plain";

                    Exception ex = context.Features.Get<IExceptionHandlerFeature>().Error;
                    if (!(ex is null))
                    {Errors
                        ILogger logger = loggerFactory.CreateLogger("ApiStudy.Api.Extensions.ExceptionHandlingExtensions");
                        logger.LogError(
                            $"Encounter error while handling request. ExceptionMsgs: {exMsgs}");
                    }
                    await context.Response.WriteAsync(ex?.Message ?? "Error occurred, but cannot get exception message.For more detail, go to see the log.");
                }));
        }

        private static Task<string> FlattenExceptionMsgAsync(Exception ex)
        {
            return Task.Run(() =>
            {
                var sb = new StringBuilder();
                Exception t = ex;
                while(!(t is null))
                {
                    sb.AppendLine($"ExceptoinType-{ex.GetType().Name}: {ex.Message}");
                    t = ex.InnerException;
                };
                return sb.ToString();
            });
        }
    }
}

再Startup.Configure中使用這個例外處理程式

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IWebHostEnviroment env)
{
    if (env.IsDevelopment()) 
    {
        app.UseCustomExceptionHandler(loggerFactory);  //modified code
    }

    //app.UseDeveloperExceptionPage();
    app.UseHsts();
    app.UseHttpsRedirection();

    app.UseMvc(); //使用默認路由
}

實作資料介面類(Resource),使用AutoMapper在Resource和Entity中映射

為Entity類創建對應的Resource類

ApiUserResource
namespace ApiStudy.Infrastructure.Resources
{
    using System;

    public class ApiUserResource
    {
        public Guid Guid { get; set; }
        public string Name { get; set; }
        //public string Passwd { get; set; }
        public DateTime RegistrationDate { get; set; }
        public DateTime Birth { get; set; }
        public string ProfilePhotoUrl { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
    }
}

使用 AutoMapper

  • 添加nuget包
    AutoMapper
    AutoMapper.Extensions.Microsoft.DependencyInjection

  • 配置映射
    可以創建Profile
    CreateMap<TSource,TDestination>()

    MappingProfile
    namespace ApiStudy.Api.Extensions
    {
        using ApiStudy.Core.Entities;
        using ApiStudy.Infrastructure.Resources;
        using AutoMapper;
        using System;
        using System.Text;
    
        public class MappingProfile : Profile
        {
            public MappingProfile()
            {
                CreateMap<ApiUser, ApiUserResource>()
                    .ForMember(
                    d => d.Passwd, 
                    opt => opt.AddTransform(s => Convert.ToBase64String(Encoding.Default.GetBytes(s))));
    
                CreateMap<ApiUserResource, ApiUser>()
                    .ForMember(
                    d => d.Passwd,
                    opt => opt.AddTransform(s => Encoding.Default.GetString(Convert.FromBase64String(s))));
            }
        }
    }
    
    
  • 注入服務 -> services.AddAutoMapper()

使用FluentValidation

FluentValidation官網

安裝Nuget包

  • FluentValidation
  • FluentValidation.AspNetCore

為每一個Resource配置驗證器

  • 繼承于AbstractValidator

    ApiUserResourceValidator
    namespace ApiStudy.Infrastructure.Resources
    {
        using FluentValidation;
    
        public class ApiUserResourceValidator : AbstractValidator<ApiUserResource>
        {
            public ApiUserResourceValidator()
            {
                RuleFor(s => s.Name)
                    .MaximumLength(80)
                    .WithName("用戶名")
                    .WithMessage("{PropertyName}的最大長度為80")
                    .NotEmpty()
                    .WithMessage("{PropertyName}不能為空!");
            }
        }
    }
    
  • 注冊到容器:services.AddTransient<>()
    services.AddTransient<IValidator<ApiUserResource>, ApiUserResourceValidator>();

實作Http Get(翻頁,過濾,排序)

基本的Get實作
[HttpGet]
public async Task<IActionResult> Get()
{
    IEnumerable<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync();

    IEnumerable<ApiUserResource> apiUserResources = 
        _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers);

    return Ok(apiUserResources);
}

[HttpGet("{guid}")]
public async Task<IActionResult> Get(string guid)
{
    ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid));

    if (apiUser is null) return NotFound();

    ApiUserResource apiUserResource = _mapper.Map<ApiUser,ApiUserResource>(apiUser);

    return Ok(apiUserResource);
}

資源命名

資源應該使用名詞,例

  • api/getusers就是不正確的.
  • GET api/users就是正確的

資源命名層次結構

  • 例如api/department/{departmentId}/emoloyees, 這就表示了 department (部門)和員工
    (employee)之前是主從關系.
  • api/department/{departmentId}/emoloyees/{employeeId},就表示了該部門下的某個員
    工.

內容協商

ASP.NET Core支持輸出和輸入兩種格式化器.

  • 用于輸出的media type放在Accept Header里,表示客戶端接受這種格式的輸出.
  • 用于輸入的media type放Content-Type Header里,表示客戶端傳進來的資料是這種格式.
  • ReturnHttpNotAcceptable設為true,如果客戶端請求不支持的資料格式,就會回傳406.
    services.AddMvc(options =>
    {
        options.ReturnHttpNotAcceptable = true;
    });
    
  • 支持輸出XML格式:options.OutputFormatters.Add(newXmlDataContractSerializerOutputFormatter());

翻頁

構造翻頁請求引數類

QueryParameters
namespace ApiStudy.Core.Entities
{
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;

    public abstract class QueryParameters : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private const int DefaultPageSize = 10;
        private const int DefaultMaxPageSize = 100;

        private int _pageIndex = 1;
        public virtual int PageIndex
        {
            get => _pageIndex;
            set => SetField(ref _pageIndex, value);
        }

        private int _pageSize = DefaultPageSize;
        public virtual int PageSize
        {
            get => _pageSize;
            set => SetField(ref _pageSize, value);
        }

        private int _maxPageSize = DefaultMaxPageSize;
        public virtual int MaxPageSize
        {
            get => _maxPageSize;
            set => SetField(ref _maxPageSize, value);
        }

        public string OrderBy { get; set; }
        public string Fields { get; set; }

        protected void SetField<TField>(
            ref TField field,in TField newValue,[CallerMemberName] string propertyName = null)
        {
            if (EqualityComparer<TField>.Default.Equals(field, newValue))
                return;
            field = newValue;
            if (propertyName == nameof(PageSize) || propertyName == nameof(MaxPageSize)) SetPageSize();
            
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            
        }

        private void SetPageSize()
        {
            if (_maxPageSize <= 0) _maxPageSize = DefaultMaxPageSize;
            if (_pageSize <= 0) _pageSize = DefaultPageSize;
            _pageSize = _pageSize > _maxPageSize ? _maxPageSize : _pageSize;
        }
    }
}
ApiUserParameters
namespace ApiStudy.Core.Entities
{
    public class ApiUserParameters:QueryParameters
    {
        public string UserName { get; set; }
    }
}

Repository實作支持翻頁請求引數的方法

Repository相關代碼
/*----- ApiUserRepository -----*/
public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters)
{
    return new PaginatedList<ApiUser>(
        parameters.PageIndex,
        parameters.PageSize,
        _context.ApiUsers.Count(),
        _context.ApiUsers.Skip(parameters.PageIndex * parameters.PageSize)
        .Take(parameters.PageSize));
}

public Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters)
{
    return Task.Run(() => GetAllApiUsers(parameters));
}

/*----- IApiUserRepository -----*/
PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters);
Task<PaginatedList<ApiUser>> GetAllApiUsersAsync(ApiUserParameters parameters);

UserController部分代碼
...

[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources = 
        _mapper.Map<IEnumerable<ApiUser>,IEnumerable<ApiUserResource>>(apiUsers);

    var meta = new
    {
        PageIndex = apiUsers.PageIndex,
        PageSize = apiUsers.PageSize,
        PageCount = apiUsers.PageCount,
        TotalItemsCount = apiUsers.TotalItemsCount,
        NextPageUrl = CreateApiUserUrl(parameters, ResourceUriType.NextPage),
        PreviousPageUrl = CreateApiUserUrl(parameters, ResourceUriType.PreviousPage)
    };
    Response.Headers.Add(
        "X-Pagination",
        JsonConvert.SerializeObject(
            meta, 
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    return Ok(apiUserResources);
}

...

private string CreateApiUserUrl(ApiUserParameters parameters,ResourceUriType uriType)
{
    var param = new ApiUserParameters
    {
        PageIndex = parameters.PageIndex,
        PageSize = parameters.PageSize
    };
    switch (uriType)
    {
        case ResourceUriType.PreviousPage:
            param.PageIndex--;
            break;
        case ResourceUriType.NextPage:
            param.PageIndex++;
            break;
        case ResourceUriType.CurrentPage:
            break;
        default:break;
    }
    return Url.Link("GetAllApiUsers", parameters);
}

PS注意,為HttpGet方法添加引數的話,在.net core2.2版本下,去掉那個ApiUserController上的 [ApiController());] 特性,否則引數傳不進來..net core3.0中據說已經修復這個問題.

搜索(過濾)

修改Repository代碼:

 public PaginatedList<ApiUser> GetAllApiUsers(ApiUserParameters parameters)
{
    IQueryable<ApiUser> query = _context.ApiUsers.AsQueryable();
    query = query.Skip(parameters.PageIndex * parameters.PageSize)
            .Take(parameters.PageSize);

    if (!string.IsNullOrEmpty(parameters.UserName))
        query = _context.ApiUsers.Where(
            x => StringComparer.OrdinalIgnoreCase.Compare(x.Name, parameters.UserName) == 0);

    return new PaginatedList<ApiUser>(
        parameters.PageIndex,
        parameters.PageSize,
        query.Count(),
        query);
}

排序

排序思路

  • 需要安裝System.Linq.Dynamic.Core

思路:

  • PropertyMappingContainer
    • PropertyMapping(ApiUserPropertyMapping)
      • MappedProperty
MappedProperty
namespace ApiStudy.Infrastructure.Services
{
    public struct MappedProperty
    {
        public MappedProperty(string name, bool revert = false)
        {
            Name = name;
            Revert = revert;
        }

        public string Name { get; set; }
        public bool Revert { get; set; }
    }
}
IPropertyMapping
namespace ApiStudy.Infrastructure.Services
{
    using System.Collections.Generic;

    public interface IPropertyMapping
    {
        Dictionary<string, List<MappedProperty>> MappingDictionary { get; }
    }
}
PropertyMapping
namespace ApiStudy.Infrastructure.Services
{
    using System.Collections.Generic;

    public abstract class PropertyMapping<TSource,TDestination> : IPropertyMapping
    {
        public Dictionary<string, List<MappedProperty>> MappingDictionary { get; }

        public PropertyMapping(Dictionary<string, List<MappedProperty>> MappingDict)
        {
            MappingDictionary = MappingDict;
        }
    }
}
IPropertyMappingContainer
namespace ApiStudy.Infrastructure.Services
{
    public interface IPropertyMappingContainer
    {
        void Register<T>() where T : IPropertyMapping, new();
        IPropertyMapping Resolve<TSource, TDestination>();
        bool ValidateMappingExistsFor<TSource, TDestination>(string fields);
    }
}
PropertyMappingContainer
namespace ApiStudy.Infrastructure.Services
{
    using System;
    using System.Linq;
    using System.Collections.Generic;

    public class PropertyMappingContainer : IPropertyMappingContainer
    {
        protected internal readonly IList<IPropertyMapping> PropertyMappings = new List<IPropertyMapping>();

        public void Register<T>() where T : IPropertyMapping, new()
        {
            if (PropertyMappings.Any(x => x.GetType() == typeof(T))) return;
            PropertyMappings.Add(new T());
        }

        public IPropertyMapping Resolve<TSource,TDestination>()
        {
            IEnumerable<PropertyMapping<TSource, TDestination>> result = PropertyMappings.OfType<PropertyMapping<TSource,TDestination>>();
            if (result.Count() > 0)
                return result.First();
            throw new InvalidCastException(
               string.Format( "Cannot find property mapping instance for {0}, {1}", typeof(TSource), typeof(TDestination)));
        }

        public bool ValidateMappingExistsFor<TSource, TDestination>(string fields)
        {
            if (string.IsNullOrEmpty(fields)) return true;

            IPropertyMapping propertyMapping = Resolve<TSource, TDestination>();

            string[] splitFields = fields.Split(',');

            foreach(string property in splitFields)
            {
                string trimmedProperty = property.Trim();
                int indexOfFirstWhiteSpace = trimmedProperty.IndexOf(' ');
                string propertyName = indexOfFirstWhiteSpace <= 0 ? trimmedProperty : trimmedProperty.Remove(indexOfFirstWhiteSpace);
                
                if (!propertyMapping.MappingDictionary.Keys.Any(x => string.Equals(propertyName,x,StringComparison.OrdinalIgnoreCase))) return false;
            }
            return true;
        }
    }
}
QueryExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using ApiStudy.Infrastructure.Services;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Dynamic.Core;

    public static class QueryExtensions
    {
        public static IQueryable<T> ApplySort<T>(
           this IQueryable<T> data,in string orderBy,in IPropertyMapping propertyMapping)
        {
            if (data =https://www.cnblogs.com/Laggage/p/= null) throw new ArgumentNullException(nameof(data));
            if (string.IsNullOrEmpty(orderBy)) return data;

            string[] splitOrderBy = orderBy.Split(',');
            foreach(string property in splitOrderBy)
            {
                string trimmedProperty = property.Trim();
                int indexOfFirstSpace = trimmedProperty.IndexOf(' ');
                bool desc = trimmedProperty.EndsWith(" desc");
                string propertyName = indexOfFirstSpace > 0 ? trimmedProperty.Remove(indexOfFirstSpace) : trimmedProperty;
                propertyName = propertyMapping.MappingDictionary.Keys.FirstOrDefault(
                    x => string.Equals(x, propertyName, StringComparison.OrdinalIgnoreCase)); //ignore case of sort property

                if (!propertyMapping.MappingDictionary.TryGetValue(
                    propertyName, out List<MappedProperty> mappedProperties))
                    throw new InvalidCastException($"key mapping for {propertyName} is missing");

                mappedProperties.Reverse();
                foreach(MappedProperty mappedProperty in mappedProperties)
                {
                    if (mappedProperty.Revert) desc = !desc;
                    data = https://www.cnblogs.com/Laggage/p/data.OrderBy($"{mappedProperty.Name} {(desc ? "descending" : "ascending")} ");
                }
            }
            return data;
        }
    }
}
UserController 部分代碼
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    var meta = new
    {
        apiUsers.PageIndex,
        apiUsers.PageSize,
        apiUsers.PageCount,
        apiUsers.TotalItemsCount,
        PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
        NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
    };
    Response.Headers.Add(
        "X-Pagination",
        JsonConvert.SerializeObject(
            meta,
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    return Ok(sortedApiUserResources);
}

private string CreateApiUserUrl(ApiUserParameters parameters, ResourceUriType uriType)
{
    var param = new {
        parameters.PageIndex,
        parameters.PageSize
    };
    switch (uriType)
    {
        case ResourceUriType.PreviousPage:
            param = new
            {
                PageIndex = parameters.PageIndex - 1,
                parameters.PageSize
            };
            break;
        case ResourceUriType.NextPage:
            param = new
            {
                PageIndex = parameters.PageIndex + 1,
                parameters.PageSize
            };
            break;
        case ResourceUriType.CurrentPage:
            break;
        default: break;
    }
    return Url.Link("GetAllApiUsers", param);
}

資源塑形(Resource shaping)

回傳 資源的指定欄位

ApiStudy.Infrastructure.Extensions.TypeExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Reflection;

    public static class TypeExtensions
    {
        public static IEnumerable<PropertyInfo> GetProeprties(this Type source, string fields = null)
        {
            List<PropertyInfo> propertyInfoList = new List<PropertyInfo>();
            if (string.IsNullOrEmpty(fields))
            {
                propertyInfoList.AddRange(source.GetProperties(BindingFlags.Public | BindingFlags.Instance));
            }
            else
            {
                string[] properties = fields.Trim().Split(',');
                foreach (string propertyName in properties)
                {
                    propertyInfoList.Add(
                        source.GetProperty(
                        propertyName.Trim(),
                        BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase));
                }
            }
            return propertyInfoList;
        }
    }
}
ApiStudy.Infrastructure.Extensions.ObjectExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Reflection;

    public static class ObjectExtensions
    {
        public static ExpandoObject ToDynamicObject(this object source, in string fields = null)
        {
            List<PropertyInfo> propertyInfoList = source.GetType().GetProeprties(fields).ToList();

            ExpandoObject expandoObject = new ExpandoObject();
            foreach (PropertyInfo propertyInfo in propertyInfoList)
            {
                try
                {
                    (expandoObject as IDictionary<string, object>).Add(
                    propertyInfo.Name, propertyInfo.GetValue(source));
                }
                catch { continue; }
            }
            return expandoObject;
        }

        internal static ExpandoObject ToDynamicObject(this object source, in IEnumerable<PropertyInfo> propertyInfos, in string fields = null)
        {
            ExpandoObject expandoObject = new ExpandoObject();
            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                try
                {
                    (expandoObject as IDictionary<string, object>).Add(
                    propertyInfo.Name, propertyInfo.GetValue(source));
                }
                catch { continue; }
            }
            return expandoObject;
        }
    }
}
ApiStudy.Infrastructure.Extensions.IEnumerableExtensions
namespace ApiStudy.Infrastructure.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Dynamic;
    using System.Linq;
    using System.Reflection;

    public static class IEnumerableExtensions
    {
        public static IEnumerable<ExpandoObject> ToDynamicObject<T>(
            this IEnumerable<T> source,in string fields = null)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));

            List<ExpandoObject> expandoObejctList = new List<ExpandoObject>();
            List<PropertyInfo> propertyInfoList = typeof(T).GetProeprties(fields).ToList();
            foreach(T x in source)
            {
                expandoObejctList.Add(x.ToDynamicObject(propertyInfoList, fields));
            }
            return expandoObejctList;
        }
    }
}
ApiStudy.Infrastructure.Services.TypeHelperServices
namespace ApiStudy.Infrastructure.Services
{
    using System.Reflection;

    public class TypeHelperServices : ITypeHelperServices
    {
        public bool HasProperties<T>(string fields)
        {
            if (string.IsNullOrEmpty(fields)) return true;

            string[] splitFields = fields.Split(',');
            foreach(string splitField in splitFields)
            {
                string proeprtyName = splitField.Trim();
                PropertyInfo propertyInfo = typeof(T).GetProperty(
                    proeprtyName, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
                if (propertyInfo == null) return false;
            }
            return true;
        }
    }
}
UserContext.GetAllApiUsers(), UserContext.Get()
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    //added code
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    //modified code
    IEnumerable<ExpandoObject> sharpedApiUserResources =
        sortedApiUserResources.ToDynamicObject(parameters.Fields);

    var meta = new
    {
        apiUsers.PageIndex,
        apiUsers.PageSize,
        apiUsers.PageCount,
        apiUsers.TotalItemsCount,
        PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
        NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
    };
    Response.Headers.Add(
        "X-Pagination",
        JsonConvert.SerializeObject(
            meta,
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    //modified code
    return Ok(sharpedApiUserResources);
}

配置回傳的json名稱風格為CamelCase

StartupDevelopment.ConfigureServices
services.AddMvc(options =>
    {
        options.ReturnHttpNotAcceptable = true;
        options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
    })
        .AddJsonOptions(options =>
        {
            //added code
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        });

HATEOAS

REST里最復雜的約束,構建成熟RESTAPI的核心

  • 可進化性,自我描述
  • 超媒體(Hypermedia,例如超鏈接)驅動如何消
    費和使用API
UserContext
private IEnumerable<LinkResource> CreateLinksForApiUser(string guid,string fields = null)
{
    List<LinkResource> linkResources = new List<LinkResource>();
    if (string.IsNullOrEmpty(fields))
    {
        linkResources.Add(
            new LinkResource(Url.Link("GetApiUser", new { guid }), "self", "get"));
    }
    else
    {
        linkResources.Add(
            new LinkResource(Url.Link("GetApiUser", new { guid, fields }), "self", "get"));
    }

    linkResources.Add(
            new LinkResource(Url.Link("DeleteApiUser", new { guid }), "self", "Get"));
    return linkResources;
}

private IEnumerable<LinkResource> CreateLinksForApiUsers(ApiUserParameters parameters,bool hasPrevious,bool hasNext)
{
    List<LinkResource> resources = new List<LinkResource>();

    resources.Add(
            new LinkResource(
                CreateApiUserUrl(parameters,ResourceUriType.CurrentPage),
                "current_page", "get"));
    if (hasPrevious)
        resources.Add(
            new LinkResource(
                CreateApiUserUrl(parameters, ResourceUriType.PreviousPage),
                "previous_page", "get"));
    if (hasNext)
        resources.Add(
            new LinkResource(
                CreateApiUserUrl(parameters, ResourceUriType.NextPage),
                "next_page", "get"));

    return resources;
}

[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    IEnumerable<ExpandoObject> shapedApiUserResources =
        sortedApiUserResources.ToDynamicObject(parameters.Fields);

    IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select(
        x =>
        {
            IDictionary<string, object> dict = x as IDictionary<string, object>;
            if(dict.Keys.Contains("guid"))
                dict.Add("links", CreateLinksForApiUser(dict["guid"] as string));
            return dict as ExpandoObject;
        });

    var result = new
    {
        value = https://www.cnblogs.com/Laggage/p/shapedApiUserResourcesWithLinks,
        links = CreateLinksForApiUsers(parameters, apiUsers.HasPreviousPage, apiUsers.HasNextPage)
    };

    var meta = new
    {
        apiUsers.PageIndex,
        apiUsers.PageSize,
        apiUsers.PageCount,
        apiUsers.TotalItemsCount,
        //PreviousPageUrl = apiUsers.HasPreviousPage ? CreateApiUserUrl(parameters, ResourceUriType.PreviousPage) : string.Empty,
        //NextPageUrl = apiUsers.HasNextPage ? CreateApiUserUrl(parameters, ResourceUriType.NextPage) : string.Empty,
    };
    Response.Headers.Add("X-Pagination",
        JsonConvert.SerializeObject(
            meta,
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    return Ok(result);
}

創建供應商特定媒體型別

  • application/vnd.mycompany.hateoas+json
    • vnd是vendor的縮寫,這一條是mime type的原則,表示這個媒體型別是供應商特定的
    • 自定義的標識,也可能還包括額外的值,這里我是用的是公司名,隨后是hateoas表示回傳的回應里面要
      包含鏈接
    • “+json”
  • 在Startup里注冊.

判斷Media Type型別

  • [FromHeader(Name = "Accept")] stringmediaType
//Startup.ConfigureServices 中注冊媒體型別
services.AddMvc(options =>
    {
        options.ReturnHttpNotAcceptable = true;
        //options.OutputFormatters.Add(new XmlDataContractSerializerOutputFormatter());
        JsonOutputFormatter formatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
        formatter.SupportedMediaTypes.Add("application/vnd.laggage.hateoas+json");
    })

// get方法中判斷媒體型別
if (mediaType == "application/json") 
    return Ok(shapedApiUserResources);
else if (mediaType == "application/vnd.laggage.hateoas+json")
{
    ...
    return;
}

注意,要是的 Action 認識 application/vnd.laggage.hateoss+json ,需要在Startup.ConfigureServices中注冊這個媒體型別,上面的代碼給出了具體操作.

UserContext
[HttpGet(Name = "GetAllApiUsers")]
public async Task<IActionResult> GetAllApiUsers(ApiUserParameters parameters,[FromHeader(Name = "Accept")] string mediaType)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    IEnumerable<ExpandoObject> shapedApiUserResources =
        sortedApiUserResources.ToDynamicObject(parameters.Fields);

    if (mediaType == "application/json") return Ok(shapedApiUserResources);
    else if (mediaType == "application/vnd.laggage.hateoas+json")
    {
        IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select(
            x =>
            {
                IDictionary<string, object> dict = x as IDictionary<string, object>;
                if (dict.Keys.Contains("guid"))
                    dict.Add("links", CreateLinksForApiUser(
                                dict.FirstOrDefault(
                                    a => string.Equals(
                                        a.Key,"guid",StringComparison.OrdinalIgnoreCase))
                                .Value.ToString()));
                return dict as ExpandoObject;
            });

        var result = new
        {
            value = https://www.cnblogs.com/Laggage/p/shapedApiUserResourcesWithLinks,
            links = CreateLinksForApiUsers(parameters, apiUsers.HasPreviousPage, apiUsers.HasNextPage)
        };

        var meta = new
        {
            apiUsers.PageIndex,
            apiUsers.PageSize,
            apiUsers.PageCount,
            apiUsers.TotalItemsCount,
        };
        Response.Headers.Add("X-Pagination",
            JsonConvert.SerializeObject(
                meta,
                new JsonSerializerSettings
                { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
        return Ok(result);
    }
    return NotFound($"Can't find resources for the given media type: [{mediaType}].");
}

[HttpGet("{guid}",Name = "GetApiUser")]
public async Task<IActionResult> Get(string guid, [FromHeader(Name = "Accept")] string mediaType , string fields = null)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(fields))
        return BadRequest("fields not exist.");

    ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid));

    if (apiUser is null) return NotFound();
    ApiUserResource apiUserResource = _mapper.Map<ApiUser, ApiUserResource>(apiUser);

    ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields);
    if (mediaType == "application/json") return Ok(shapedApiUserResource);

    else if(mediaType == "application/vnd.laggage.hateoas+json")
    {

    
    IDictionary<string, object> shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary<string, object>;
    shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields));

    return Ok(shapedApiUserResourceWithLink);
    }
    return NotFound(@"Can't find resource for the given media type: [{mediaType}].");
}

  • 自定義Action約束.
RequestHeaderMatchingMediaTypeAttribute
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchingMediaTypeAttribute : Attribute, IActionConstraint
{
    private readonly string _requestHeaderToMatch;
    private readonly string[] _mediaTypes;

    public RequestHeaderMatchingMediaTypeAttribute(string requestHeaderToMatch, string[] mediaTypes)
    {
        _requestHeaderToMatch = requestHeaderToMatch;
        _mediaTypes = mediaTypes;
    }

    public bool Accept(ActionConstraintContext context)
    {
        var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
        if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
        {
            return false;
        }

        foreach (var mediaType in _mediaTypes)
        {
            var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
                mediaType, StringComparison.OrdinalIgnoreCase);
            if (mediaTypeMatches)
            {
                return true;
            }
        }

        return false;
    }

    public int Order { get; } = 0;
}
UserContext
[HttpGet(Name = "GetAllApiUsers")]
[RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })]
public async Task<IActionResult> GetHateoas(ApiUserParameters parameters)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    IEnumerable<ExpandoObject> shapedApiUserResources =
        sortedApiUserResources.ToDynamicObject(parameters.Fields);

    IEnumerable<ExpandoObject> shapedApiUserResourcesWithLinks = shapedApiUserResources.Select(
            x =>
            {
                IDictionary<string, object> dict = x as IDictionary<string, object>;
                if (dict.Keys.Contains("guid"))
                    dict.Add("links", CreateLinksForApiUser(
                                dict.FirstOrDefault(
                                    a => string.Equals(
                                        a.Key,"guid",StringComparison.OrdinalIgnoreCase))
                                .Value.ToString()));
                return dict as ExpandoObject;
            });

    var result = new
    {
        value = https://www.cnblogs.com/Laggage/p/shapedApiUserResourcesWithLinks,
        links = CreateLinksForApiUsers(parameters, apiUsers.HasPreviousPage, apiUsers.HasNextPage)
    };

    var meta = new
    {
        apiUsers.PageIndex,
        apiUsers.PageSize,
        apiUsers.PageCount,
        apiUsers.TotalItemsCount,
    };
    Response.Headers.Add("X-Pagination",
        JsonConvert.SerializeObject(
            meta,
            new JsonSerializerSettings
            { ContractResolver = new CamelCasePropertyNamesContractResolver() }));
    return Ok(result);
}

[HttpGet(Name = "GetAllApiUsers")]
[RequestHeaderMatchingMediaType("Accept",new string[] { "application/json" })]
public async Task<IActionResult> Get(ApiUserParameters parameters)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(parameters.Fields))
        return BadRequest("fields not exist.");

    if (!_propertyMappingContainer.ValidateMappingExistsFor<ApiUserResource, ApiUser>(parameters.OrderBy))
        return BadRequest("can't find fields for sorting.");

    PaginatedList<ApiUser> apiUsers = await _apiUserRepository.GetAllApiUsersAsync(parameters);

    IEnumerable<ApiUserResource> apiUserResources =
        _mapper.Map<IEnumerable<ApiUser>, IEnumerable<ApiUserResource>>(apiUsers);

    IEnumerable<ApiUserResource> sortedApiUserResources =
        apiUserResources.AsQueryable().ApplySort(
            parameters.OrderBy, _propertyMappingContainer.Resolve<ApiUserResource, ApiUser>());

    IEnumerable<ExpandoObject> shapedApiUserResources =
        sortedApiUserResources.ToDynamicObject(parameters.Fields);

    return Ok(shapedApiUserResources);
}

[HttpGet("{guid}", Name = "GetApiUser")]
[RequestHeaderMatchingMediaType("Accept", new string[] { "application/vnd.laggage.hateoas+json" })]
public async Task<IActionResult> GetHateoas(string guid, string fields = null)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(fields))
        return BadRequest("fields not exist.");

    ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid));

    if (apiUser is null) return NotFound();
    ApiUserResource apiUserResource = _mapper.Map<ApiUser, ApiUserResource>(apiUser);

    ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields);

    IDictionary<string, object> shapedApiUserResourceWithLink = shapedApiUserResource as IDictionary<string, object>;
    shapedApiUserResourceWithLink.Add("links", CreateLinksForApiUser(guid, fields));

    return Ok(shapedApiUserResourceWithLink);
}

[HttpGet("{guid}", Name = "GetApiUser")]
[RequestHeaderMatchingMediaType("Accept", new string[] { "application/json" })]
public async Task<IActionResult> Get(string guid,  string fields = null)
{
    if (!_typeHelper.HasProperties<ApiUserResource>(fields))
        return BadRequest("fields not exist.");

    ApiUser apiUser = await _apiUserRepository.GetApiUserByGuidAsync(Guid.Parse(guid));

    if (apiUser is null) return NotFound();
    ApiUserResource apiUserResource = _mapper.Map<ApiUser, ApiUserResource>(apiUser);

    ExpandoObject shapedApiUserResource = apiUserResource.ToDynamicObject(fields);

    return Ok(shapedApiUserResource);
}

Post添加資源

Post - 不安全,非冪等
要回傳添加好的資源,并且回傳頭中有獲得新創建資源的連接.

安全性和冪等性

  • 安全性是指方法執行后并不會改變資源的表述
  • 冪等性是指方法無論執行多少次都會得到同樣
    的結果

代碼實作

StartUp中注冊Fluent,用于驗證

services.AddMvc(...)
        .AddFluentValidation();

services.AddTransient<IValidator<ApiUserAddResource>, ApiUserAddResourceValidator>();
ApiStudy.Infrastructure.Resources.ApiUserAddResourceValidator
namespace ApiStudy.Infrastructure.Resources
{
    using FluentValidation;

    public class ApiUserAddResourceValidator : AbstractValidator<ApiUserAddResource>
    {
        public ApiUserAddResourceValidator()
        {
            RuleFor(x => x.Name)
                .MaximumLength(20)                
                .WithName("用戶名")
                .WithMessage("{PropertyName}的最大長度為20!")
                .NotNull()
                .WithMessage("{PropertyName}是必填的!")
                .NotEmpty()
                .WithMessage("{PropertyName}不能為空!");
            RuleFor(x => x.Passwd)
                .NotNull()
                .WithName("密碼")
                .WithMessage("{PropertyName}是必填的!")
                .MinimumLength(6)
                .WithMessage("{PropertyName}的最小長度是6")
                .MaximumLength(16)
                .WithMessage("{PropertyName}的最大長度是16");
            RuleFor(x => x.PhoneNumber)
                .NotNull()
                .WithName("電話")
                .WithMessage("{PropertyName}是必填的!")
                .NotEmpty()
                .WithMessage("{PropertyName}不能為空!");
        }
    }
}
UserContext.AddApiUser()
[HttpPost(Name = "CreateApiUser")]
[RequestHeaderMatchingMediaType("Content-Type",new string[] { "application/vnd.laggage.create.apiuser+json" })]
[RequestHeaderMatchingMediaType("Accept",new string[] { "application/vnd.laggage.hateoas+json" })]
public async Task<IActionResult> AddUser([FromBody] ApiUserAddResource apiUser)
{
    if (!ModelState.IsValid)
        return UnprocessableEntity(ModelState);

    ApiUser newUser = _mapper.Map<ApiUser>(apiUser);
    newUser.Guid = Guid.NewGuid();
    newUser.ProfilePhotoUrl = $"www.eample.com/photo/{newUser.Guid}";
    newUser.RegistrationDate = DateTime.Now;

    await _apiUserRepository.AddApiUserAsync(newUser);
    if (!await _unitOfWork.SaveChangesAsync())
        throw new Exception("Failed to save changes");

    IDictionary<string, object> shapedUserResource = 
        _mapper.Map<ApiUserResource>(newUser)
        .ToDynamicObject() as IDictionary<string, object>;
    IEnumerable<LinkResource> links = CreateLinksForApiUser(newUser.Guid.ToString());
    shapedUserResource.Add("links", links);
    return CreatedAtRoute("GetApiUser",new { newUser.Guid }, shapedUserResource);
}

Delete

  • 引數 : ID
  • 冪等的
    • 多次請求的副作用和單次請求的副作用是一樣的.每次發送了DELETE請求之后,服務器的狀態都是一樣的.
  • 不安全
ApiStudy.Api.Controllers.UserController
[HttpDelete("{guid}",Name = "DeleteApiUser")]
public async Task<IActionResult> DeleteApiUser(string guid)
{
    ApiUser userToDelete = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid));

    if (userToDelete == null) return NotFound();

    await _apiUserRepository.DeleteApiUserAsync(userToDelete);
    if (!await _unitOfWork.SaveChangesAsync())
        throw new Exception("Failed to delete apiUser");

    return NoContent();
}

PUT & PATCH

相關類:

ApiStudy.Infrastructure.Resources.ApiUserAddOrUpdateResource
namespace ApiStudy.Infrastructure.Resources
{
    using System;

    public abstract class ApiUserAddOrUpdateResource
    {
        public string Name { get; set; }
        public string Passwd { get; set; }
        public DateTime Birth { get; set; }
        public string PhoneNumber { get; set; }
        public string Email { get; set; }
    }
}
ApiStudy.Infrastructure.Resources.ApiUserAddResource
namespace ApiStudy.Infrastructure.Resources
{
    public class ApiUserAddResource:ApiUserAddOrUpdateResource
    {
    }
}
ApiStudy.Infrastructure.Resources.ApiUserUpdateResource
namespace ApiStudy.Infrastructure.Resources
{
    public class ApiUserUpdateResource : ApiUserAddOrUpdateResource
    {
    }
}
ApiStudy.Infrastructure.Resources.ApiUserAddOrUpdateResourceValidator
namespace ApiStudy.Infrastructure.Resources
{
    using FluentValidation;

    public class ApiUserAddOrUpdateResourceValidator<T> : AbstractValidator<T> where T: ApiUserAddOrUpdateResource
    {
        public ApiUserAddOrUpdateResourceValidator()
        {
            RuleFor(x => x.Name)
                .MaximumLength(20)                
                .WithName("用戶名")
                .WithMessage("{PropertyName}的最大長度為20!")
                .NotNull()
                .WithMessage("{PropertyName}是必填的!")
                .NotEmpty()
                .WithMessage("{PropertyName}不能為空!");
            RuleFor(x => x.Passwd)
                .NotNull()
                .WithName("密碼")
                .WithMessage("{PropertyName}是必填的!")
                .MinimumLength(6)
                .WithMessage("{PropertyName}的最小長度是6")
                .MaximumLength(16)
                .WithMessage("{PropertyName}的最大長度是16");
            RuleFor(x => x.PhoneNumber)
                .NotNull()
                .WithName("電話")
                .WithMessage("{PropertyName}是必填的!")
                .NotEmpty()
                .WithMessage("{PropertyName}不能為空!");
        }
    }
}

PUT 整體更新

  • 回傳204
  • 引數
    • ID,
    • [FromBody]XxxxUpdateResource
ApiStudy.Api.Controllers.UpdateApiUser
[HttpPut("{guid}",Name = "PutApiUser")]
public async Task<IActionResult> UpdateApiUser(string guid,[FromBody] ApiUserUpdateResource apiUserUpdateResource)
{
    if (!ModelState.IsValid) return BadRequest(ModelState);

    ApiUser userToUpdate = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid));
    if (userToUpdate == null) return NotFound();

    _mapper.Map(apiUserUpdateResource, userToUpdate);

    if (!await _unitOfWork.SaveChangesAsync())
        throw new Exception("Failed to update Entity of ApiUser");
    return NoContent();
}

PATCH

  • Content-Type
    • application/json-patch+json
  • 回傳204
  • 引數
    • ID
    • [FromBody] JsonPatchDocument
  • op操作
    • 添加:{“op”: "add", "path": "/xxx", "value": "xxx"},如果該屬性不存,那么就添加該屬性,如
      果屬性存在,就改變屬性的值,這個對靜態型別不適用,
    • 洗掉:{“op”: "remove", "path": "/xxx"},洗掉某個屬性,或把它設為默認值(例如空值),
    • 替換:{“op”: "replace", "path": "/xxx", "value": "xxx"},改變屬性的值,也可以理解為先執行
      了洗掉,然后進行添加,
    • 復制:{“op”: "copy", "from": "/xxx", "path": "/yyy"},把某個屬性的值賦給目標屬性,
    • 移動:{“op”: "move", "from": "/xxx", "path": "/yyy"},把源屬性的值賦值給目標屬性,并把源
      屬性洗掉或設成默認值,
    • 測驗:{“op”: "test", "path": "/xxx", "value": "xxx"},測驗目標屬性的值和指定的值是一樣的,
  • path,資源的屬性名
    • 可以有層級結構
  • value 更新的值
[
    {
        "op":"replace",
        "path":"/name",
        "value":"阿黃"
    },
    {
        "op":"remove",
        "path":"/email"
    }
]
ApiStudy.Api.Controllers.UserContext.UpdateApiUser
[HttpPatch("{guid}",Name = "PatchApiUser")]
[RequestHeaderMatchingMediaType("Content-Type",new string[] { "application/vnd.laggage.patch.apiuser+json" })]
public async Task<IActionResult> UpdateApiUser(
    string guid,[FromBody] JsonPatchDocument<ApiUserUpdateResource> userUpdateDoc)
{
    if (userUpdateDoc == null) return BadRequest();

    ApiUser userToUpdate = await _apiUserRepository.GetApiUserByGuidAsync(new Guid(guid));
    if (userToUpdate is null) return NotFound();

    ApiUserUpdateResource userToUpdateResource = _mapper.Map<ApiUserUpdateResource>(userToUpdate);
    userUpdateDoc.ApplyTo(userToUpdateResource);

    _mapper.Map(userToUpdateResource, userToUpdate);
    if (!await _unitOfWork.SaveChangesAsync()) throw new Exception("Failed to update Entity of ApiUser");

    return NoContent();
}

Http常用方法總結

  • GET(獲取資源):
    • GET api/countries,回傳200,集合資料;找不到資料回傳404,
    • GET api/countries/{id},回傳200,單個資料;找不到回傳404.
  • DELETE(洗掉資源)
    • DELETE api/countries/{id},成功204;沒找到資源404,
    • DELETE api/countries,很少用,也是204或者404.
  • POST (創建資源):
    • POST api/countries,成功回傳201和單個資料;如果資源沒有創建則回傳404
    • POST api/countries/{id},肯定不會成功,回傳404或409.
    • POST api/countrycollections,成功回傳201和集合;沒創建資源則回傳404
  • PUT (整體更新):
    • PUT api/countries/{id},成功可以回傳200,204;沒找到資源則回傳404
    • PUT api/countries,集合操作很少見,回傳200,204或404
  • PATCH(區域更新):
    • PATCHapi/countries/{id},200單個資料,204或者404
    • PATCHapi/countries,集合操作很少見,回傳200集合,204或404.

轉載請註明出處,本文鏈接:https://www.uj5u.com/net/105808.html

標籤:.NET Core

上一篇:觸摸陽光隊作品簡介

下一篇:abp(net core)+easyui+efcore實作倉儲管理系統——EasyUI之貨物管理四 (二十二)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • WebAPI簡介

    Web體系結構: 有三個核心:資源(resource),URL(統一資源識別符號)和表示 他們的關系是這樣的:一個資源由一個URL進行標識,HTTP客戶端使用URL定位資源,表示是從資源回傳資料,媒體型別是資源回傳的資料格式。 接下來我們說下HTTP. HTTP協議的系統是一種無狀態的方式,使用請求/ ......

    uj5u.com 2020-09-09 22:07:47 more
  • asp.net core 3.1 入口:Program.cs中的Main函式

    本文分析Program.cs 中Main()函式中代碼的運行順序分析asp.net core程式的啟動,重點不是剖析原始碼,而是理清程式開始時執行的順序。到呼叫了哪些實體,哪些法方。asp.net core 3.1 的程式入口在專案Program.cs檔案里,如下。ususing System; us ......

    uj5u.com 2020-09-09 22:07:49 more
  • asp.net網站作為websocket服務端的應用該如何寫

    最近被websocket的一個問題困擾了很久,有一個需求是在web網站中搭建websocket服務。客戶端通過網頁與服務器建立連接,然后服務器根據ip給客戶端網頁發送資訊。 其實,這個需求并不難,只是剛開始對websocket的內容不太了解。上網搜索了一下,有通過asp.net core 實作的、有 ......

    uj5u.com 2020-09-09 22:08:02 more
  • ASP.NET 開源匯入匯出庫Magicodes.IE Docker中使用

    Magicodes.IE在Docker中使用 更新歷史 2019.02.13 【Nuget】版本更新到2.0.2 【匯入】修復單列匯入的Bug,單元測驗“OneColumnImporter_Test”。問題見(https://github.com/dotnetcore/Magicodes.IE/is ......

    uj5u.com 2020-09-09 22:08:05 more
  • 在webform中使用ajax

    如果你用過Asp.net webform, 說明你也算是.NET 開發的老兵了。WEBform應該是2011 2013左右,當時還用visual studio 2005、 visual studio 2008。后來基本都用的是MVC。 如果是新開發的專案,估計沒人會用webform技術。但是有些舊版 ......

    uj5u.com 2020-09-09 22:08:50 more
  • iis添加asp.net網站,訪問提示:由于擴展配置問題而無法提供您請求的

    今天在iis服務器配置asp.net網站,遇到一個問題,記錄一下: 問題:由于擴展配置問題而無法提供您請求的頁面。如果該頁面是腳本,請添加處理程式。如果應下載檔案,請添加 MIME 映射。 WindowServer2012服務器,添加角色安裝完.netframework和iis之后,運行aspx頁面 ......

    uj5u.com 2020-09-09 22:10:00 more
  • WebAPI-處理架構

    帶著問題去思考,大家好! 問題1:HTTP請求和回傳相應的HTTP回應資訊之間發生了什么? 1:首先是最底層,托管層,位于WebAPI和底層HTTP堆疊之間 2:其次是 訊息處理程式管道層,這里比如日志和快取。OWIN的參考是將訊息處理程式管道的一些功能下移到堆疊下端的OWIN中間件了。 3:控制器處理 ......

    uj5u.com 2020-09-09 22:11:13 more
  • 微信門戶開發框架-使用指導說明書

    微信門戶應用管理系統,采用基于 MVC + Bootstrap + Ajax + Enterprise Library的技術路線,界面層采用Boostrap + Metronic組合的前端框架,資料訪問層支持Oracle、SQLServer、MySQL、PostgreSQL等資料庫。框架以MVC5,... ......

    uj5u.com 2020-09-09 22:15:18 more
  • WebAPI-HTTP編程模型

    帶著問題去思考,大家好!它是什么?它包含什么?它能干什么? 訊息 HTTP編程模型的核心就是訊息抽象,表示為:HttPRequestMessage,HttpResponseMessage.用于客戶端和服務端之間交換請求和回應訊息。 HttpMethod類包含了一組靜態屬性: private stat ......

    uj5u.com 2020-09-09 22:15:23 more
  • 部署WebApi隨筆

    一、跨域 NuGet參考Microsoft.AspNet.WebApi.Cors WebApiConfig.cs中配置: // Web API 配置和服務 config.EnableCors(new EnableCorsAttribute("*", "*", "*")); 二、清除默認回傳XML格式 ......

    uj5u.com 2020-09-09 22:15:48 more
最新发布
  • C#多執行緒學習(二) 如何操縱一個執行緒

    <a href="https://www.cnblogs.com/x-zhi/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2943582/20220801082530.png" alt="" /></...

    uj5u.com 2023-04-19 09:17:20 more
  • C#多執行緒學習(二) 如何操縱一個執行緒

    C#多執行緒學習(二) 如何操縱一個執行緒 執行緒學習第一篇:C#多執行緒學習(一) 多執行緒的相關概念 下面我們就動手來創建一個執行緒,使用Thread類創建執行緒時,只需提供執行緒入口即可。(執行緒入口使程式知道該讓這個執行緒干什么事) 在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的 ......

    uj5u.com 2023-04-19 09:16:49 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    <a href="https://www.cnblogs.com/huangxincheng/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/214741/20200614104537.png" alt="" /&g...

    uj5u.com 2023-04-18 08:39:04 more
  • 記一次 .NET某醫療器械清洗系統 卡死分析

    一:背景 1. 講故事 前段時間協助訓練營里的一位朋友分析了一個程式卡死的問題,回過頭來看這個案例比較經典,這篇稍微整理一下供后來者少踩坑吧。 二:WinDbg 分析 1. 為什么會卡死 因為是表單程式,理所當然就是看主執行緒此時正在做什么? 可以用 ~0s ; k 看一下便知。 0:000> k # ......

    uj5u.com 2023-04-18 08:33:10 more
  • SignalR, No Connection with that ID,IIS

    <a href="https://www.cnblogs.com/smartstar/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/u36196.jpg" alt="" /></a>...

    uj5u.com 2023-03-30 17:21:52 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:15:33 more
  • 一次對pool的誤用導致的.net頻繁gc的診斷分析

    <a href="https://www.cnblogs.com/dotnet-diagnostic/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/3115652/20230225090434.png" alt=""...

    uj5u.com 2023-03-28 10:13:31 more
  • C#遍歷指定檔案夾中所有檔案的3種方法

    <a href="https://www.cnblogs.com/xbhp/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/957602/20230310105611.png" alt="" /></a&...

    uj5u.com 2023-03-27 14:46:55 more
  • C#/VB.NET:如何將PDF轉為PDF/A

    <a href="https://www.cnblogs.com/Carina-baby/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/2859233/20220427162558.png" alt="" />...

    uj5u.com 2023-03-27 14:46:35 more
  • 武裝你的WEBAPI-OData聚合查詢

    <a href="https://www.cnblogs.com/podolski/" target="_blank"><img width="48" height="48" class="pfs" src="https://pic.cnblogs.com/face/616093/20140323000327.png" alt="" /><...

    uj5u.com 2023-03-27 14:46:16 more