鏈接:https://github.com/solenovex/asp.net-web-api-2.2-starter-template
簡介
這個是我自己編寫的asp.net web api 2.2的基礎框架,使用了Entity Framework 6.2(beta)作為ORM。
該模板主要采用了 Unit of Work 和 Repository 模式,使用autofac進行控制反轉(ioc)。
記錄Log采用的是NLog。
結構
項目列表如下圖:
該啟動模板為多層結構,其結構如下圖:
開發流程
1. 創建model
在LegacyApplication.Models項目里建立相應的文件夾作為子模塊,然后創建model,例如Nationality.cs:

using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.Infrastructure.Annotations; using LegacyApplication.Shared.Features.Base; namespace LegacyApplication.Models.HumanResources { public class Nationality : EntityBase { public string Name { get; set; } } public class NationalityConfiguration : EntityBaseConfiguration<Nationality> { public NationalityConfiguration() { ToTable("hr.Nationality"); Property(x => x.Name).IsRequired().HasMaxLength(50); Property(x => x.Name).HasMaxLength(50).HasColumnAnnotation( IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute { IsUnique = true })); } } }
所建立的model需要使用EntityBase作為基類,EntityBase有幾個業務字段,包括CreateUser,CreateTime,UpdateUser,UpdateTime,LastAction。EntityBase代碼如下:

using System; namespace LegacyApplication.Shared.Features.Base { public class EntityBase : IEntityBase { public EntityBase(string userName = "匿名") { CreateTime = UpdateTime = DateTime.Now; LastAction = "創建"; CreateUser = UpdateUser = userName; } public int Id { get; set; } public DateTime CreateTime { get; set; } public DateTime UpdateTime { get; set; } public string CreateUser { get; set; } public string UpdateUser { get; set; } public string LastAction { get; set; } public int Order { get; set; } } }
model需要使用Fluent Api來配置數據庫的映射屬性等,按約定使用Model名+Configuration作為fluent api的類的名字,並需要繼承EntityBaseConfiguration<T>這個類,這個類對EntityBase的幾個屬性進行了映射配置,其代碼如下:

using System.Data.Entity.ModelConfiguration; namespace LegacyApplication.Shared.Features.Base { public class EntityBaseConfiguration<T> : EntityTypeConfiguration<T> where T : EntityBase { public EntityBaseConfiguration() { HasKey(e => e.Id); Property(x => x.CreateTime).IsRequired(); Property(x => x.UpdateTime).IsRequired(); Property(x => x.CreateUser).IsRequired().HasMaxLength(50); Property(x => x.UpdateUser).IsRequired().HasMaxLength(50); Property(x => x.LastAction).IsRequired().HasMaxLength(50); } } }
1.1 自成樹形的Model
自成樹形的model是指自己和自己成主外鍵關系的Model(表),例如菜單表或者部門表的設計有時候是這樣的,下面以部門為例:

using System.Collections.Generic; using LegacyApplication.Shared.Features.Tree; namespace LegacyApplication.Models.HumanResources { public class Department : TreeEntityBase<Department> { public string Name { get; set; } public ICollection<Employee> Employees { get; set; } } public class DepartmentConfiguration : TreeEntityBaseConfiguration<Department> { public DepartmentConfiguration() { ToTable("hr.Department"); Property(x => x.Name).IsRequired().HasMaxLength(100); } } }
與普通的Model不同的是,它需要繼承的是TreeEntityBase<T>這個基類,TreeEntityBase<T>的代碼如下:

using System.Collections.Generic; using LegacyApplication.Shared.Features.Base; namespace LegacyApplication.Shared.Features.Tree { public class TreeEntityBase<T>: EntityBase, ITreeEntity<T> where T: TreeEntityBase<T> { public int? ParentId { get; set; } public string AncestorIds { get; set; } public bool IsAbstract { get; set; } public int Level => AncestorIds?.Split('-').Length ?? 0; public T Parent { get; set; } public ICollection<T> Children { get; set; } } }
其中ParentId,Parent,Children這幾個屬性是樹形關系相關的屬性,AncestorIds定義為所有祖先Id層級別連接到一起的一個字符串,需要自己實現。然后Level屬性是通過AncestorIds這個屬性自動獲取該Model在樹形結構里面的層級。
該Model的fluent api配置類需要繼承的是TreeEntityBaseConfiguration<T>這個類,代碼如下:

using System.Collections.Generic; using LegacyApplication.Shared.Features.Base; namespace LegacyApplication.Shared.Features.Tree { public class TreeEntityBaseConfiguration<T> : EntityBaseConfiguration<T> where T : TreeEntityBase<T> { public TreeEntityBaseConfiguration() { Property(x => x.AncestorIds).HasMaxLength(200); Ignore(x => x.Level); HasOptional(x => x.Parent).WithMany(x => x.Children).HasForeignKey(x => x.ParentId).WillCascadeOnDelete(false); } } }
針對樹形結構的model,我還做了幾個簡單的Extension Methods,代碼如下:

using System; using System.Collections.Generic; using System.Linq; namespace LegacyApplication.Shared.Features.Tree { public static class TreeExtensions { /// <summary> /// 把樹形結構數據的集合轉化成單一根結點的樹形結構數據 /// </summary> /// <typeparam name="T">樹形結構實體</typeparam> /// <param name="items">樹形結構實體的集合</param> /// <returns>樹形結構實體的根結點</returns> public static TreeEntityBase<T> ToSingleRoot<T>(this IEnumerable<TreeEntityBase<T>> items) where T : TreeEntityBase<T> { var all = items.ToList(); if (!all.Any()) { return null; } var top = all.Where(x => x.ParentId == null).ToList(); if (top.Count > 1) { throw new Exception("樹的根節點數大於1個"); } if (top.Count == 0) { throw new Exception("未能找到樹的根節點"); } TreeEntityBase<T> root = top.Single(); Action<TreeEntityBase<T>> findChildren = null; findChildren = current => { var children = all.Where(x => x.ParentId == current.Id).ToList(); foreach (var child in children) { findChildren(child); } current.Children = children as ICollection<T>; }; findChildren(root); return root; } /// <summary> /// 把樹形結構數據的集合轉化成多個根結點的樹形結構數據 /// </summary> /// <typeparam name="T">樹形結構實體</typeparam> /// <param name="items">樹形結構實體的集合</param> /// <returns>多個樹形結構實體根結點的集合</returns> public static List<TreeEntityBase<T>> ToMultipleRoots<T>(this IEnumerable<TreeEntityBase<T>> items) where T : TreeEntityBase<T> { List<TreeEntityBase<T>> roots; var all = items.ToList(); if (!all.Any()) { return null; } var top = all.Where(x => x.ParentId == null).ToList(); if (top.Any()) { roots = top; } else { throw new Exception("未能找到樹的根節點"); } Action<TreeEntityBase<T>> findChildren = null; findChildren = current => { var children = all.Where(x => x.ParentId == current.Id).ToList(); foreach (var child in children) { findChildren(child); } current.Children = children as ICollection<T>; }; roots.ForEach(findChildren); return roots; } /// <summary> /// 作為父節點, 取得樹形結構實體的祖先ID串 /// </summary> /// <typeparam name="T">樹形結構實體</typeparam> /// <param name="parent">父節點實體</param> /// <returns></returns> public static string GetAncestorIdsAsParent<T>(this T parent) where T : TreeEntityBase<T> { return string.IsNullOrEmpty(parent.AncestorIds) ? parent.Id.ToString() : (parent.AncestorIds + "-" + parent.Id); } } }
2. 把Model加入到DbContext里面
建立完Model后,需要把Model加入到Context里面,下面是CoreContext的代碼:

using System; using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; using System.Diagnostics; using System.Reflection; using LegacyApplication.Database.Infrastructure; using LegacyApplication.Models.Core; using LegacyApplication.Models.HumanResources; using LegacyApplication.Models.Work; using LegacyApplication.Shared.Configurations; namespace LegacyApplication.Database.Context { public class CoreContext : DbContext, IUnitOfWork { public CoreContext() : base(AppSettings.DefaultConnection) { //System.Data.Entity.Database.SetInitializer<CoreContext>(null); #if DEBUG Database.Log = Console.Write; Database.Log = message => Trace.WriteLine(message); #endif } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); //去掉默認開啟的級聯刪除 modelBuilder.Configurations.AddFromAssembly(Assembly.GetAssembly(typeof(UploadedFile))); } //Core public DbSet<UploadedFile> UploadedFiles { get; set; } //Work public DbSet<InternalMail> InternalMails { get; set; } public DbSet<InternalMailTo> InternalMailTos { get; set; } public DbSet<InternalMailAttachment> InternalMailAttachments { get; set; } public DbSet<Todo> Todos { get; set; } public DbSet<Schedule> Schedules { get; set; } //HR public DbSet<Department> Departments { get; set; } public DbSet<Employee> Employees { get; set; } public DbSet<JobPostLevel> JobPostLevels { get; set; } public DbSet<JobPost> JobPosts { get; set; } public DbSet<AdministrativeLevel> AdministrativeLevels { get; set; } public DbSet<TitleLevel> TitleLevels { get; set; } public DbSet<Nationality> Nationalitys { get; set; } } }
其中“modelBuilder.Configurations.AddFromAssembly(Assembly.GetAssembly(typeof(UploadedFile)));” 會把UploadFile所在的Assembly(也就是LegacyApplication.Models這個項目)里面所有的fluent api配置類(EntityTypeConfiguration的派生類)全部加載進來。
這里說一下CoreContext,由於它派生與DbContext,而DbContext本身就實現了Unit of Work 模式,所以我做Unit of work模式的時候,就不考慮重新建立一個新類作為Unit of work了,我從DbContext抽取了幾個方法,提煉出了IUnitofWork接口,代碼如下:

using System; using System.Threading; using System.Threading.Tasks; namespace LegacyApplication.Database.Infrastructure { public interface IUnitOfWork: IDisposable { int SaveChanges(); Task<int> SaveChangesAsync(CancellationToken cancellationToken); Task<int> SaveChangesAsync(); } }
用的時候IUnitOfWork就是CoreContext的化身。
3.建立Repository
我理解的Repository(百貨)里面應該具有各種小粒度的邏輯方法,以便復用,通常Repository里面要包含各種單筆和多筆的CRUD方法。
此外,我在我的模板里做了約定,不在Repository里面進行任何的提交保存等動作。
下面我們來建立一個Repository,就用Nationality為例,在LegacyApplication.Repositories里面相應的文件夾建立NationalityRepository類:

using LegacyApplication.Database.Infrastructure; using LegacyApplication.Models.HumanResources; namespace LegacyApplication.Repositories.HumanResources { public interface INationalityRepository : IEntityBaseRepository<Nationality> { } public class NationalityRepository : EntityBaseRepository<Nationality>, INationalityRepository { public NationalityRepository(IUnitOfWork unitOfWork) : base(unitOfWork) { } } }
代碼很簡單,但是它已經包含了常見的10多種CRUD方法,因為它繼承於EntityBaseRepository這個泛型類,這個類的代碼如下:

using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using LegacyApplication.Database.Context; using LegacyApplication.Shared.Features.Base; namespace LegacyApplication.Database.Infrastructure { public class EntityBaseRepository<T> : IEntityBaseRepository<T> where T : class, IEntityBase, new() { #region Properties protected CoreContext Context { get; } public EntityBaseRepository(IUnitOfWork unitOfWork) { Context = unitOfWork as CoreContext; } #endregion public virtual IQueryable<T> All => Context.Set<T>(); public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = Context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query; } public virtual int Count() { return Context.Set<T>().Count(); } public async Task<int> CountAsync() { return await Context.Set<T>().CountAsync(); } public T GetSingle(int id) { return Context.Set<T>().FirstOrDefault(x => x.Id == id); } public async Task<T> GetSingleAsync(int id) { return await Context.Set<T>().FirstOrDefaultAsync(x => x.Id == id); } public T GetSingle(Expression<Func<T, bool>> predicate) { return Context.Set<T>().FirstOrDefault(predicate); } public async Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate) { return await Context.Set<T>().FirstOrDefaultAsync(predicate); } public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = Context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query.Where(predicate).FirstOrDefault(); } public async Task<T> GetSingleAsync(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = Context.Set<T>(); foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return await query.Where(predicate).FirstOrDefaultAsync(); } public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate) { return Context.Set<T>().Where(predicate); } public virtual void Add(T entity) { DbEntityEntry dbEntityEntry = Context.Entry<T>(entity); Context.Set<T>().Add(entity); } public virtual void Update(T entity) { DbEntityEntry<T> dbEntityEntry = Context.Entry<T>(entity); dbEntityEntry.Property(x => x.Id).IsModified = false; dbEntityEntry.State = EntityState.Modified; dbEntityEntry.Property(x => x.CreateUser).IsModified = false; dbEntityEntry.Property(x => x.CreateTime).IsModified = false; } public virtual void Delete(T entity) { DbEntityEntry dbEntityEntry = Context.Entry<T>(entity); dbEntityEntry.State = EntityState.Deleted; } public virtual void AddRange(IEnumerable<T> entities) { Context.Set<T>().AddRange(entities); } public virtual void DeleteRange(IEnumerable<T> entities) { foreach (var entity in entities) { DbEntityEntry dbEntityEntry = Context.Entry<T>(entity); dbEntityEntry.State = EntityState.Deleted; } } public virtual void DeleteWhere(Expression<Func<T, bool>> predicate) { IEnumerable<T> entities = Context.Set<T>().Where(predicate); foreach (var entity in entities) { Context.Entry<T>(entity).State = EntityState.Deleted; } } public void Attach(T entity) { Context.Set<T>().Attach(entity); } public void AttachRange(IEnumerable<T> entities) { foreach (var entity in entities) { Attach(entity); } } public void Detach(T entity) { Context.Entry<T>(entity).State = EntityState.Detached; } public void DetachRange(IEnumerable<T> entities) { foreach (var entity in entities) { Detach(entity); } } public void AttachAsModified(T entity) { Attach(entity); Update(entity); } } }
我相信這個泛型類你們都應該能看明白,如果不明白可以@我。通過繼承這個類,所有的Repository都具有了常見的方法,並且寫的代碼很少。
但是為什么自己建立的Repository不直接繼承與EntityBaseRepository,而是中間非得插一層接口呢?因為我的Repository可能還需要其他的自定義方法,這些自定義方法需要提取到這個接口里面以便使用。
3.1 對Repository進行注冊
在LegacyApplication.Web項目里App_Start/MyConfigurations/AutofacWebapiConfig.cs里面對Repository進行ioc注冊,我使用的是AutoFac:

using System.Reflection; using System.Web.Http; using Autofac; using Autofac.Integration.WebApi; using LegacyApplication.Database.Context; using LegacyApplication.Database.Infrastructure; using LegacyApplication.Repositories.Core; using LegacyApplication.Repositories.HumanResources; using LegacyApplication.Repositories.Work; using LegacyApplication.Services.Core; using LegacyApplication.Services.Work; namespace LegacyStandalone.Web.MyConfigurations { public class AutofacWebapiConfig { public static IContainer Container; public static void Initialize(HttpConfiguration config) { Initialize(config, RegisterServices(new ContainerBuilder())); } public static void Initialize(HttpConfiguration config, IContainer container) { config.DependencyResolver = new AutofacWebApiDependencyResolver(container); } private static IContainer RegisterServices(ContainerBuilder builder) { builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); //builder.RegisterType<CoreContext>() // .As<DbContext>() // .InstancePerRequest(); builder.RegisterType<CoreContext>().As<IUnitOfWork>().InstancePerRequest(); //Services builder.RegisterType<CommonService>().As<ICommonService>().InstancePerRequest(); builder.RegisterType<InternalMailService>().As<IInternalMailService>().InstancePerRequest(); //Core builder.RegisterType<UploadedFileRepository>().As<IUploadedFileRepository>().InstancePerRequest(); //Work builder.RegisterType<InternalMailRepository>().As<IInternalMailRepository>().InstancePerRequest(); builder.RegisterType<InternalMailToRepository>().As<IInternalMailToRepository>().InstancePerRequest(); builder.RegisterType<InternalMailAttachmentRepository>().As<IInternalMailAttachmentRepository>().InstancePerRequest(); builder.RegisterType<TodoRepository>().As<ITodoRepository>().InstancePerRequest(); builder.RegisterType<ScheduleRepository>().As<IScheduleRepository>().InstancePerRequest(); //HR builder.RegisterType<DepartmentRepository>().As<IDepartmentRepository>().InstancePerRequest(); builder.RegisterType<EmployeeRepository>().As<IEmployeeRepository>().InstancePerRequest(); builder.RegisterType<JobPostLevelRepository>().As<IJobPostLevelRepository>().InstancePerRequest(); builder.RegisterType<JobPostRepository>().As<IJobPostRepository>().InstancePerRequest(); builder.RegisterType<AdministrativeLevelRepository>().As<IAdministrativeLevelRepository>().InstancePerRequest(); builder.RegisterType<TitleLevelRepository>().As<ITitleLevelRepository>().InstancePerRequest(); builder.RegisterType<NationalityRepository>().As<INationalityRepository>().InstancePerRequest(); Container = builder.Build(); return Container; } } }
在里面我們也可以看見我把CoreContext注冊為IUnitOfWork。
4.建立ViewModel
ViewModel是最終和前台打交道的一層。所有的Model都是轉化成ViewModel之后再傳送到前台,所有前台提交過來的對象數據,大多是作為ViewModel傳進來的。
下面舉一個例子:

using System.ComponentModel.DataAnnotations; using LegacyApplication.Shared.Features.Base; namespace LegacyApplication.ViewModels.HumanResources { public class NationalityViewModel : EntityBase { [Display(Name = "名稱")] [Required(ErrorMessage = "{0}是必填項")] [StringLength(50, ErrorMessage = "{0}的長度不可超過{1}")] public string Name { get; set; } } }
同樣,它要繼承EntityBase類。
同時,ViewModel里面應該加上屬性驗證的注解,例如DisplayName,StringLength,Range等等等等,加上注解的屬性在ViewModel從前台傳進來的時候會進行驗證(詳見Controller部分)。
4.1注冊ViewModel和Model之間的映射
由於ViewModel和Model之間經常需要轉化,如果手寫代碼的話,那就太多了。所以我這里采用了一個主流的.net庫叫AutoMapper。
因為映射有兩個方法,所以每對需要注冊兩次,分別在DomainToViewModelMappingProfile.cs和ViewModelToDomainMappingProfile.cs里面:

using System.Linq; using AutoMapper; using LegacyApplication.Models.Core; using LegacyApplication.Models.HumanResources; using LegacyApplication.Models.Work; using LegacyApplication.ViewModels.Core; using LegacyApplication.ViewModels.HumanResources; using LegacyStandalone.Web.Models; using Microsoft.AspNet.Identity.EntityFramework; using LegacyApplication.ViewModels.Work; namespace LegacyStandalone.Web.MyConfigurations.Mapping { public class DomainToViewModelMappingProfile : Profile { public override string ProfileName => "DomainToViewModelMappings"; public DomainToViewModelMappingProfile() { CreateMap<ApplicationUser, UserViewModel>(); CreateMap<IdentityRole, RoleViewModel>(); CreateMap<IdentityUserRole, RoleViewModel>(); CreateMap<UploadedFile, UploadedFileViewModel>(); CreateMap<InternalMail, InternalMailViewModel>(); CreateMap<InternalMailTo, InternalMailToViewModel>(); CreateMap<InternalMailAttachment, InternalMailAttachmentViewModel>(); CreateMap<InternalMail, SentMailViewModel>() .ForMember(dest => dest.AttachmentCount, opt => opt.MapFrom(ori => ori.Attachments.Count)) .ForMember(dest => dest.HasAttachments, opt => opt.MapFrom(ori => ori.Attachments.Any())) .ForMember(dest => dest.ToCount, opt => opt.MapFrom(ori => ori.Tos.Count)) .ForMember(dest => dest.AnyoneRead, opt => opt.MapFrom(ori => ori.Tos.Any(y => y.HasRead))) .ForMember(dest => dest.AllRead, opt => opt.MapFrom(ori => ori.Tos.All(y => y.HasRead))); CreateMap<Todo, TodoViewModel>(); CreateMap<Schedule, ScheduleViewModel>(); CreateMap<Department, DepartmentViewModel>() .ForMember(dest => dest.Parent, opt => opt.Ignore()) .ForMember(dest => dest.Children, opt => opt.Ignore()); CreateMap<Employee, EmployeeViewModel>(); CreateMap<JobPostLevel, JobPostLevelViewModel>(); CreateMap<JobPost, JobPostViewModel>(); CreateMap<AdministrativeLevel, AdministrativeLevelViewModel>(); CreateMap<TitleLevel, TitleLevelViewModel>(); CreateMap<Nationality, NationalityViewModel>(); } } }

using AutoMapper; using LegacyApplication.Models.Core; using LegacyApplication.Models.HumanResources; using LegacyApplication.Models.Work; using LegacyApplication.ViewModels.Core; using LegacyApplication.ViewModels.HumanResources; using LegacyStandalone.Web.Models; using Microsoft.AspNet.Identity.EntityFramework; using LegacyApplication.ViewModels.Work; namespace LegacyStandalone.Web.MyConfigurations.Mapping { public class ViewModelToDomainMappingProfile : Profile { public override string ProfileName => "ViewModelToDomainMappings"; public ViewModelToDomainMappingProfile() { CreateMap<UserViewModel, ApplicationUser>(); CreateMap<RoleViewModel, IdentityRole>(); CreateMap<RoleViewModel, IdentityUserRole>(); CreateMap<UploadedFileViewModel, UploadedFile>(); CreateMap<InternalMailViewModel, InternalMail>(); CreateMap<InternalMailToViewModel, InternalMailTo>(); CreateMap<InternalMailAttachmentViewModel, InternalMailAttachment>(); CreateMap<TodoViewModel, Todo>(); CreateMap<ScheduleViewModel, Schedule>(); CreateMap<DepartmentViewModel, Department>() .ForMember(dest => dest.Parent, opt => opt.Ignore()) .ForMember(dest => dest.Children, opt => opt.Ignore()); CreateMap<EmployeeViewModel, Employee>(); CreateMap<JobPostLevelViewModel, JobPostLevel>(); CreateMap<JobPostViewModel, JobPost>(); CreateMap<AdministrativeLevelViewModel, AdministrativeLevel>(); CreateMap<TitleLevelViewModel, TitleLevel>(); CreateMap<NationalityViewModel, Nationality>(); } } }
高級功能還是要參考AutoMapper的文檔。
5.建立Controller
先上個例子:

using System.Collections.Generic; using System.Data.Entity; using System.Threading.Tasks; using System.Web.Http; using AutoMapper; using LegacyApplication.Database.Infrastructure; using LegacyApplication.Models.HumanResources; using LegacyApplication.Repositories.HumanResources; using LegacyApplication.ViewModels.HumanResources; using LegacyStandalone.Web.Controllers.Bases; using LegacyApplication.Services.Core; namespace LegacyStandalone.Web.Controllers.HumanResources { [RoutePrefix("api/Nationality")] public class NationalityController : ApiControllerBase { private readonly INationalityRepository _nationalityRepository; public NationalityController( INationalityRepository nationalityRepository, ICommonService commonService, IUnitOfWork unitOfWork) : base(commonService, unitOfWork) { _nationalityRepository = nationalityRepository; } public async Task<IEnumerable<NationalityViewModel>> Get() { var models = await _nationalityRepository.All.ToListAsync(); var viewModels = Mapper.Map<IEnumerable<Nationality>, IEnumerable<NationalityViewModel>>(models); return viewModels; } public async Task<IHttpActionResult> GetOne(int id) { var model = await _nationalityRepository.GetSingleAsync(id); if (model != null) { var viewModel = Mapper.Map<Nationality, NationalityViewModel>(model); return Ok(viewModel); } return NotFound(); } public async Task<IHttpActionResult> Post([FromBody]NationalityViewModel viewModel) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var newModel = Mapper.Map<NationalityViewModel, Nationality>(viewModel); newModel.CreateUser = newModel.UpdateUser = User.Identity.Name; _nationalityRepository.Add(newModel); await UnitOfWork.SaveChangesAsync(); return RedirectToRoute("", new { controller = "Nationality", id = newModel.Id }); } public async Task<IHttpActionResult> Put(int id, [FromBody]NationalityViewModel viewModel) { if (!ModelState.IsValid) { return BadRequest(ModelState); } viewModel.UpdateUser = User.Identity.Name; viewModel.UpdateTime = Now; viewModel.LastAction = "更新"; var model = Mapper.Map<NationalityViewModel, Nationality>(viewModel); _nationalityRepository.AttachAsModified(model); await UnitOfWork.SaveChangesAsync(); return Ok(viewModel); } public async Task<IHttpActionResult> Delete(int id) { var model = await _nationalityRepository.GetSingleAsync(id); if (model == null) { return NotFound(); } _nationalityRepository.Delete(model); await UnitOfWork.SaveChangesAsync(); return Ok(); } } }
這是比較標准的Controller,里面包含一個多筆查詢,一個單筆查詢和CUD方法。
所有的Repository,Service等都是通過依賴注入弄進來的。
所有的Controller需要繼承ApiControllerBase,所有Controller公用的方法、屬性(property)等都應該放在ApiControllerBase里面,其代碼如下:

namespace LegacyStandalone.Web.Controllers.Bases { public abstract class ApiControllerBase : ApiController { protected readonly ICommonService CommonService; protected readonly IUnitOfWork UnitOfWork; protected readonly IDepartmentRepository DepartmentRepository; protected readonly IUploadedFileRepository UploadedFileRepository; protected ApiControllerBase( ICommonService commonService, IUnitOfWork untOfWork) { CommonService = commonService; UnitOfWork = untOfWork; DepartmentRepository = commonService.DepartmentRepository; UploadedFileRepository = commonService.UploadedFileRepository; } #region Current Information protected DateTime Now => DateTime.Now; protected string UserName => User.Identity.Name; protected ApplicationUserManager UserManager => Request.GetOwinContext().GetUserManager<ApplicationUserManager>(); [NonAction] protected async Task<ApplicationUser> GetMeAsync() { var me = await UserManager.FindByNameAsync(UserName); return me; } [NonAction] protected async Task<Department> GetMyDepartmentEvenNull() { var department = await DepartmentRepository.GetSingleAsync(x => x.Employees.Any(y => y.No == UserName)); return department; } [NonAction] protected async Task<Department> GetMyDepartmentNotNull() { var department = await GetMyDepartmentEvenNull(); if (department == null) { throw new Exception("您不屬於任何單位/部門"); } return department; } #endregion #region Upload [NonAction] public virtual async Task<IHttpActionResult> Upload() { var root = GetUploadDirectory(DateTime.Now.ToString("yyyyMM")); var result = await UploadFiles(root); return Ok(result); } [NonAction] public virtual async Task<IHttpActionResult> GetFileAsync(int fileId) { var model = await UploadedFileRepository.GetSingleAsync(x => x.Id == fileId); if (model != null) { return new FileActionResult(model); } return null; } [NonAction] public virtual IHttpActionResult GetFileByPath(string path) { return new FileActionResult(path); } [NonAction] protected string GetUploadDirectory(params string[] subDirectories) { #if DEBUG var root = HttpContext.Current.Server.MapPath("~/App_Data/Upload"); #else var root = AppSettings.UploadDirectory; #endif if (subDirectories != null && subDirectories.Length > 0) { foreach (var t in subDirectories) { root = Path.Combine(root, t); } } if (!Directory.Exists(root)) { Directory.CreateDirectory(root); } return root; } [NonAction] protected async Task<List<UploadedFile>> UploadFiles(string root) { var list = await UploadFilesAsync(root); var models = Mapper.Map<List<UploadedFileViewModel>, List<UploadedFile>>(list).ToList(); foreach (var model in models) { UploadedFileRepository.Add(model); } await UnitOfWork.SaveChangesAsync(); return models; } [NonAction] private async Task<List<UploadedFileViewModel>> UploadFilesAsync(string root) { if (!Request.Content.IsMimeMultipartContent()) { throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); } var provider = new MultipartFormDataStreamProvider(root); var count = HttpContext.Current.Request.Files.Count; var files = new List<HttpPostedFile>(count); for (var i = 0; i < count; i++) { files.Add(HttpContext.Current.Request.Files[i]); } await Request.Content.ReadAsMultipartAsync(provider); var list = new List<UploadedFileViewModel>(); var now = DateTime.Now; foreach (var file in provider.FileData) { var temp = file.Headers.ContentDisposition.FileName; var length = temp.Length; var lastSlashIndex = temp.LastIndexOf(@"\", StringComparison.Ordinal); var fileName = temp.Substring(lastSlashIndex + 2, length - lastSlashIndex - 3); var fileInfo = files.SingleOrDefault(x => x.FileName == fileName); long size = 0; if (fileInfo != null) { size = fileInfo.ContentLength; } var newFile = new UploadedFileViewModel { FileName = fileName, Path = file.LocalFileName, Size = size, Deleted = false }; var userName = string.IsNullOrEmpty(User.Identity?.Name) ? "anonymous" : User.Identity.Name; newFile.CreateUser = newFile.UpdateUser = userName; newFile.CreateTime = newFile.UpdateTime = now; newFile.LastAction = "上傳"; list.Add(newFile); } return list; } #endregion protected override void Dispose(bool disposing) { base.Dispose(disposing); UserManager?.Dispose(); UnitOfWork?.Dispose(); } } #region Upload Model internal class FileActionResult : IHttpActionResult { private readonly bool _isInline = false; private readonly string _contentType; public FileActionResult(UploadedFile fileModel, string contentType, bool isInline = false) { UploadedFile = fileModel; _contentType = contentType; _isInline = isInline; } public FileActionResult(UploadedFile fileModel) { UploadedFile = fileModel; } public FileActionResult(string path) { UploadedFile = new UploadedFile { Path = path }; } private UploadedFile UploadedFile { get; set; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { FileStream file; try { file = File.OpenRead(UploadedFile.Path); } catch (DirectoryNotFoundException) { return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); } catch (FileNotFoundException) { return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); } var response = new HttpResponseMessage { Content = new StreamContent(file) }; var name = UploadedFile.FileName ?? file.Name; var last = name.LastIndexOf("\\", StringComparison.Ordinal); if (last > -1) { var length = name.Length - last - 1; name = name.Substring(last + 1, length); } if (!string.IsNullOrEmpty(_contentType)) { response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(_contentType); } response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue(_isInline ? DispositionTypeNames.Inline : DispositionTypeNames.Attachment) { FileName = HttpUtility.UrlEncode(name, Encoding.UTF8) }; return Task.FromResult(response); } } #endregion }
這個基類里面可以有很多東西,目前,它可以獲取當前用戶名,當前時間,當前用戶(ApplicationUser),當前登陸人的部門,文件上傳下載等。
這個基類保證的通用方法的可擴展性和復用性,其他例如EntityBase,EntityBaseRepository等等也都是這個道理。
注意,前面在Repository里面講過,我們不在Repository里面做提交動作。
所以所有的提交動作都在Controller里面進行,通常所有掛起的更改只需要一次提交即可,畢竟Unit of Work模式。
5.1獲取枚舉的Controller
所有的枚舉都應該放在LegacyApplication.Shared/ByModule/xxx模塊/Enums下。
然后前台通過訪問"api/Shared"(SharedController.cs)獲取該模塊下(或者整個項目)所有的枚舉。

using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; namespace LegacyStandalone.Web.Controllers.Bases { [RoutePrefix("api/Shared")] public class SharedController : ApiController { [HttpGet] [Route("Enums/{moduleName?}")] public IHttpActionResult GetEnums(string moduleName = null) { var exp = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(t => t.GetTypes()) .Where(t => t.IsEnum); if (!string.IsNullOrEmpty(moduleName)) { exp = exp.Where(x => x.Namespace == $"LegacyApplication.Shared.ByModule.{moduleName}.Enums"); } var enumTypes = exp; var result = new Dictionary<string, Dictionary<string, int>>(); foreach (var enumType in enumTypes) { result[enumType.Name] = Enum.GetValues(enumType).Cast<int>().ToDictionary(e => Enum.GetName(enumType, e), e => e); } return Ok(result); } [HttpGet] [Route("EnumsList/{moduleName?}")] public IHttpActionResult GetEnumsList(string moduleName = null) { var exp = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(t => t.GetTypes()) .Where(t => t.IsEnum); if (!string.IsNullOrEmpty(moduleName)) { exp = exp.Where(x => x.Namespace == $"LegacyApplication.Shared.ByModule.{moduleName}.Enums"); } var enumTypes = exp; var result = new Dictionary<string, List<KeyValuePair<string, int>>>(); foreach (var e in enumTypes) { var names = Enum.GetNames(e); var values = Enum.GetValues(e).Cast<int>().ToArray(); var count = names.Count(); var list = new List<KeyValuePair<string, int>>(count); for (var i = 0; i < count; i++) { list.Add(new KeyValuePair<string, int> (names[i], values[i])); } result.Add(e.Name, list); } return Ok(result); } } }
6.建立Services
注意Controller里面的CommonService就處在Service層。並不是所有的Model/Repository都有相應的Service層。
通常我在如下情況會建立Service:
a.需要寫與數據庫操作無關的可復用邏輯方法。
b.需要寫多個Repository參與的可復用的邏輯方法或引用。
我的CommonService就是b這個類型,其代碼如下:

using LegacyApplication.Repositories.Core; using LegacyApplication.Repositories.HumanResources; using System; using System.Collections.Generic; using System.Text; namespace LegacyApplication.Services.Core { public interface ICommonService { IUploadedFileRepository UploadedFileRepository { get; } IDepartmentRepository DepartmentRepository { get; } } public class CommonService : ICommonService { public IUploadedFileRepository UploadedFileRepository { get; } public IDepartmentRepository DepartmentRepository { get; } public CommonService( IUploadedFileRepository uploadedFileRepository, IDepartmentRepository departmentRepository) { UploadedFileRepository = uploadedFileRepository; } } }
因為我每個Controller都需要注入這幾個Repository,所以如果不寫service的話,每個Controller的Constructor都需要多幾行代碼,所以我把他們封裝進了一個Service,然后注入這個Service就行。
Service也需要進行IOC注冊。
7.其他
a.使用自行實現的異常處理和異常記錄類:

GlobalConfiguration.Configuration.Services.Add(typeof(IExceptionLogger), new MyExceptionLogger()); GlobalConfiguration.Configuration.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler());
b.啟用了Cors
c.所有的Controller默認是需要驗證的
d.采用Token Bearer驗證方式
e.默認建立一個用戶,在DatabaseInitializer.cs里面可以看見用戶名密碼。
f.EF采用Code First,需要手動進行遷移。(我認為這樣最好)
g.內置把漢字轉為拼音首字母的工具,PinyinTools
h.所有上傳文件的Model需要實現IFileEntity接口,參考代碼中的例子。
i.所有后台翻頁返回的結果應該是使用PaginatedItemsViewModel。
里面有很多例子,請參考。
注意:項目啟動后顯示錯誤頁,因為我把Home頁去掉了。請訪問/Help頁查看API列表。
過些日子可以考慮加入Swagger。