在一些性能要求很高的應用中,使用protocol buffer序列化,優於Json。並且protocol buffer向後兼容的能力比較好。git
因爲Asp.net core 採用了全新的MiddleWare方式,所以使用protobuf序列化,只須要使用Protobuf-net修飾須要序列化的對象,並在MVC初始化的時候增長相應的Formatter就能夠了。github
MVC Controller 的Action返回對象時,MVC回根據用戶的Request Header裏面的MIME選擇對應的Formater來序列化返回對象( Serialize returned object)。MVC具備默認的Json Formater,這個能夠不用管。數據庫
這裏有一個直接能夠運行的例子,具備Server和Client代碼 https://github.com/damienbod/AspNetMvc6ProtobufFormattersjson
可是,這裏面有一個很嚴重的問題。 看下面的例子。app
例如咱們須要序列化的對象時ApparatusType,服務端的定義(使用了EntityFramework)是這樣的:async
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using ProtoBuf; namespace Hammergo.Data { [ProtoContract] public partial class ApparatusType { public ApparatusType() { this.Apps = new List<App>(); } [ProtoMember(1)] public System.Guid Id { get; set; } [ProtoMember(2)] [MaxLength(20)] public string TypeName { get; set; } [ProtoIgnore] public virtual ICollection<App> Apps { get; set; } } }
屬於ProtoBuf 的三個修飾爲ide
[ProtoContract]
[ProtoMember(1)]
[ProtoMember(2)]
其餘的不用管,在客戶端定義是這樣的
using System; using System.Collections.Generic; using ProtoBuf; namespace Hammergo.Poco { [ProtoContract] public class ApparatusType { [ProtoMember(1)] public virtual System.Guid Id { get; set; } [ProtoMember(2)] public virtual string TypeName { get; set; } } }
這裏使用了Virtual關鍵字,是爲了生成POCO的代理類,以跟蹤狀態,沒有這個要求能夠不使用。性能
若是使用https://github.com/damienbod/AspNetMvc6ProtobufFormatters 的方案就會發現測試
若是ASP.NET 的action返回List<AppratusType>,在客戶端使用ui
var result = response.Content.ReadAsAsync<List<Hammergo.Poco.ApparatusType>>(new[] { new ProtoBufFormatter() }).Result;
就會拋出異常,ReadAsAsync ProtoBuf Formatter No MediaTypeFormatter is available to read
大意是沒有 相應的MediaTypeFormatter來供ReadAsAsync來使用,
檢查https://github.com/damienbod/AspNetMvc6ProtobufFormatters 的方案,發現它調用了https://github.com/WebApiContrib/WebApiContrib.Formatting.ProtoBuf裏面的ProtoBufFormatter.cs ,這個裏面有一個錯誤。
using System; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Reflection; using System.Threading.Tasks; using ProtoBuf; using ProtoBuf.Meta; namespace WebApiContrib.Formatting { public class ProtoBufFormatter : MediaTypeFormatter { private static readonly MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/x-protobuf"); private static Lazy<RuntimeTypeModel> model = new Lazy<RuntimeTypeModel>(CreateTypeModel); public static RuntimeTypeModel Model { get { return model.Value; } } public ProtoBufFormatter() { SupportedMediaTypes.Add(mediaType); } public static MediaTypeHeaderValue DefaultMediaType { get { return mediaType; } } public override bool CanReadType(Type type) { return CanReadTypeCore(type); } public override bool CanWriteType(Type type) { return CanReadTypeCore(type); } public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger) { var tcs = new TaskCompletionSource<object>(); try { object result = Model.Deserialize(stream, null, type); tcs.SetResult(result); } catch (Exception ex) { tcs.SetException(ex); } return tcs.Task; } public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext) { var tcs = new TaskCompletionSource<object>(); try { Model.Serialize(stream, value); tcs.SetResult(null); } catch (Exception ex) { tcs.SetException(ex); } return tcs.Task; } private static RuntimeTypeModel CreateTypeModel() { var typeModel = TypeModel.Create(); typeModel.UseImplicitZeroDefaults = false; return typeModel; } private static bool CanReadTypeCore(Type type) { return type.GetCustomAttributes(typeof(ProtoContractAttribute)).Any(); } } }
private static bool CanReadTypeCore(Type type)這個有問題,它只能識別有ProtoContract的類,無法識別其對應的IEnumerable<T>,修改這個方法就能夠了。以下:
private static bool CanReadTypeCore(Type type) { bool isCan = type.GetCustomAttributes(typeof(ProtoContractAttribute)).Any(); if (!isCan && typeof(IEnumerable).IsAssignableFrom(type)) { var temp = type.GetGenericArguments().FirstOrDefault(); isCan = temp.GetCustomAttributes(typeof(ProtoContractAttribute)).Any(); } return isCan; }
下面我給出,關鍵的代碼片斷:
使用了一個輔助Library,結構以下圖:
DateTimeOffsetSurrogate.cs的代碼以下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ProtoBuf; namespace ProtoBufHelper { [ProtoContract] public class DateTimeOffsetSurrogate { [ProtoMember(1)] public long DateTimeTicks { get; set; } [ProtoMember(2)] public short OffsetMinutes { get; set; } public static implicit operator DateTimeOffsetSurrogate(DateTimeOffset value) { return new DateTimeOffsetSurrogate { DateTimeTicks = value.Ticks, OffsetMinutes = (short)value.Offset.TotalMinutes }; } public static implicit operator DateTimeOffset(DateTimeOffsetSurrogate value) { return new DateTimeOffset(value.DateTimeTicks, TimeSpan.FromMinutes(value.OffsetMinutes)); } } }
ProtoBufFormatter.cs 的代碼以下:
using System; using System.Collections; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Formatting; using System.Net.Http.Headers; using System.Reflection; using System.Threading.Tasks; using ProtoBuf; using ProtoBuf.Meta; namespace ProtoBufHelper { public class ProtoBufFormatter : MediaTypeFormatter { private static readonly MediaTypeHeaderValue mediaType = new MediaTypeHeaderValue("application/x-protobuf"); private static Lazy<RuntimeTypeModel> model = new Lazy<RuntimeTypeModel>(CreateTypeModel); public static RuntimeTypeModel Model { get { return model.Value; } } public ProtoBufFormatter() { SupportedMediaTypes.Add(mediaType); } public static MediaTypeHeaderValue DefaultMediaType { get { return mediaType; } } public override bool CanReadType(Type type) { var temp = CanReadTypeCore(type); return temp; } public override bool CanWriteType(Type type) { return CanReadTypeCore(type); } public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger) { var tcs = new TaskCompletionSource<object>(); try { object result = Model.Deserialize(stream, null, type); tcs.SetResult(result); } catch (Exception ex) { tcs.SetException(ex); } return tcs.Task; } public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext) { var tcs = new TaskCompletionSource<object>(); try { Model.Serialize(stream, value); tcs.SetResult(null); } catch (Exception ex) { tcs.SetException(ex); } return tcs.Task; } private static RuntimeTypeModel CreateTypeModel() { var typeModel = TypeModel.Create(); typeModel.UseImplicitZeroDefaults = false; typeModel.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate)); return typeModel; } private static bool CanReadTypeCore(Type type) { bool isCan = type.GetCustomAttributes(typeof(ProtoContractAttribute)).Any(); if (!isCan && typeof(IEnumerable).IsAssignableFrom(type)) { var temp = type.GetGenericArguments().FirstOrDefault(); isCan = temp.GetCustomAttributes(typeof(ProtoContractAttribute)).Any(); } return isCan; } } }
這樣就能夠設置ASP.NET Core端的代碼:
添加ProtobufInputFormatter.cs 和 ProtobufOutputFormatter.cs 代碼分別以下:
using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; using ProtoBuf.Meta; using ProtoBufHelper; namespace DamService { public class ProtobufInputFormatter : InputFormatter { private static Lazy<RuntimeTypeModel> model = new Lazy<RuntimeTypeModel>(CreateTypeModel); public static RuntimeTypeModel Model { get { return model.Value; } } public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context) { var type = context.ModelType; var request = context.HttpContext.Request; MediaTypeHeaderValue requestContentType = null; MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType); object result = Model.Deserialize(context.HttpContext.Request.Body, null, type); return InputFormatterResult.SuccessAsync(result); } public override bool CanRead(InputFormatterContext context) { return true; } private static RuntimeTypeModel CreateTypeModel() { var typeModel = TypeModel.Create(); typeModel.UseImplicitZeroDefaults = false; typeModel.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate)); return typeModel; } } }
using System; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; using ProtoBuf.Meta; using ProtoBufHelper; namespace DamService { public class ProtobufOutputFormatter : OutputFormatter { private static Lazy<RuntimeTypeModel> model = new Lazy<RuntimeTypeModel>(CreateTypeModel); public string ContentType { get; private set; } public static RuntimeTypeModel Model { get { return model.Value; } } public ProtobufOutputFormatter() { ContentType = "application/x-protobuf"; SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/x-protobuf")); //SupportedEncodings.Add(Encoding.GetEncoding("utf-8")); } private static RuntimeTypeModel CreateTypeModel() { var typeModel = TypeModel.Create(); typeModel.UseImplicitZeroDefaults = false; typeModel.Add(typeof(DateTimeOffset), false).SetSurrogate(typeof(DateTimeOffsetSurrogate)); return typeModel; } public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context) { var response = context.HttpContext.Response; Model.Serialize(response.Body, context.Object); return Task.FromResult(response); } } }
在Startup.cs中
public void ConfigureServices(IServiceCollection services) 方法中這樣添加MVC中間件
services.AddMvc(options =>
{
options.InputFormatters.Add(new ProtobufInputFormatter());
options.OutputFormatters.Add(new ProtobufOutputFormatter());
options.FormatterMappings.SetMediaTypeMappingForFormat("protobuf", MediaTypeHeaderValue.Parse("application/x-protobuf"));
});
整個Startup.cs代碼
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using DamService.Data; using DamService.Models; using DamService.Services; using System.Net; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace DamService { public class Startup { public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); if (env.IsDevelopment()) { // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 builder.AddUserSecrets(); } builder.AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Add framework services. services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddScoped(provider => new Hammergo.Data.DamWCFContext(Configuration.GetConnectionString("OdataDBConnectionString"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.AddMvc(options => { options.InputFormatters.Add(new ProtobufInputFormatter()); options.OutputFormatters.Add(new ProtobufOutputFormatter()); options.FormatterMappings.SetMediaTypeMappingForFormat("protobuf", MediaTypeHeaderValue.Parse("application/x-protobuf")); }); // Add application services. //services.AddTransient<IEmailSender, AuthMessageSender>(); //services.AddTransient<ISmsSender, AuthMessageSender>(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); //if (env.IsDevelopment()) //{ // app.UseDeveloperExceptionPage(); // app.UseDatabaseErrorPage(); // app.UseBrowserLink(); //} //else //{ // app.UseExceptionHandler("/Home/Error"); //} app.UseExceptionHandler(_exceptionHandler); app.UseStaticFiles(); app.UseIdentity(); // Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715 app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } private void _exceptionHandler(IApplicationBuilder builder) { builder.Run( async context => { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; context.Response.ContentType = "text/plain"; var error = context.Features.Get<IExceptionHandlerFeature>(); if (error != null) { await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false); } }); } } }
上面的
services.AddScoped(provider => new Hammergo.Data.DamWCFContext(Configuration.GetConnectionString("OdataDBConnectionString")));是我本身的數據庫鏈接,可使用本身的,也能夠不用,我用的是EntityFrameWork 6.1.3 不是core,目前core還有一些功能沒有,暫時不使用。
添加一個測試用的Controller,
using System; using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Hammergo.Data; using System.Linq.Expressions; using System.Data.Entity; using System.Collections.Generic; using System.Threading.Tasks; namespace DamService.Controllers { public class AppsController : Controller { private readonly DamWCFContext _dbContext; private readonly ILogger _logger; public AppsController( DamWCFContext dbContext, ILoggerFactory loggerFactory) { _dbContext = dbContext; _logger = loggerFactory.CreateLogger<AccountController>(); } [HttpGet] public async Task<List<App>> Top10() { return await _dbContext.Apps.Take(10).ToListAsync(); } } }
客戶端測試代碼:
var client = new HttpClient { BaseAddress = new Uri("http://localhost.Fiddler:40786/") }; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/x-protobuf")); HttpResponseMessage response = null; //test top 10 string uri = "Apps/Top10"; Trace.WriteLine("\n Test {0}.", uri); response = client.GetAsync(uri).Result; if (response.IsSuccessStatusCode) { var result = response.Content.ReadAsAsync<List<Hammergo.Poco.App>>(new[] { new ProtoBufFormatter() }).Result; Assert.AreEqual(result.Count, 10, "反序列化失敗"); Console.WriteLine("{0} test success!", uri); } else { var message = response.Content.ReadAsStringAsync().Result; Console.WriteLine("{0} ({1})\n message: {2} ", (int)response.StatusCode, response.ReasonPhrase, message); }
http://localhost.Fiddler:40786/ 這裏40786爲服務端口,Fiddler表示使用了Fiddler代理,這樣在使用時須要開啓Fiddler,若是不使用Fidller,將URI修改成:
http://localhost:40786/