链接: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();
Property(x => x.Name).HasMaxLength().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();
Property(x => x.UpdateUser).IsRequired().HasMaxLength();
Property(x => x.LastAction).IsRequired().HasMaxLength();
}
}
}

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();
}
}
}

 

与普通的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 ?? ;
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();
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 > )
{
throw new Exception("树的根节点数大于1个");
}
if (top.Count == )
{
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(, 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 > )
{
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 = ; 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 + , length - lastSlashIndex - );
var fileInfo = files.SingleOrDefault(x => x.FileName == fileName);
long size = ;
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 > -)
{
var length = name.Length - last - ;
name = name.Substring(last + , 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 = ; 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。

asp.net web api 2.2 基础框架(带例子)的更多相关文章

  1. 基于SpringBoot的Web API快速开发基础框架

    其实还是很因为懒,才会有这个案例项目的产生,每次开启一个终端的小服务都要整理一次框架,造成重复的.不必要的.缺乏创造性的劳动,SO,本着可以用.用着简单的原则上传代码到Github,希望有需要的朋友直 ...

  2. 【ASP.NET Web API教程】2.3 与实体框架一起使用Web API

    原文:[ASP.NET Web API教程]2.3 与实体框架一起使用Web API 2.3 Using Web API with Entity Framework 2.3 与实体框架一起使用Web ...

  3. ASP.NET Web API 开篇示例介绍

    ASP.NET Web API 开篇示例介绍 ASP.NET Web API 对于我这个初学者来说ASP.NET Web API这个框架很陌生又熟悉着. 陌生的是ASP.NET Web API是一个全 ...

  4. Asp.net web api 知多少

    本系列主要翻译自<ASP.NET MVC Interview Questions and Answers >- By Shailendra Chauhan,想看英文原版的可访问http:/ ...

  5. 使用 OWIN 作为 ASP.NET Web API 的宿主

    使用 OWIN 作为 ASP.NET Web API 的宿主 ASP.NET Web API 是一种框架,用于轻松构建可以访问多种客户端(包括浏览器和移动 设备)的 HTTP 服务. ASP.NET ...

  6. 【转】WCF和ASP.NET Web API在应用上的选择

    文章出处:http://www.cnblogs.com/shanyou/archive/2012/09/26/2704814.html 在最近发布的Visual Studio 2012及.NET 4. ...

  7. ASP.NET Web API下的HttpController激活:程序集的解析

    ASP.NET Web API下的HttpController激活:程序集的解析 HttpController的激活是由处于消息处理管道尾端的HttpRoutingDispatcher来完成的,具体来 ...

  8. WCF和ASP.NET Web API在应用上的选择

    小分享:我有几张阿里云优惠券,用券购买或者升级阿里云相应产品最多可以优惠五折!领券地址:https://promotion.aliyun.com/ntms/act/ambassador/shareto ...

  9. [转载]WCF和ASP.NET Web API在应用上的选择

    http://www.cnblogs.com/shanyou/archive/2012/09/26/2704814.html http://msdn.microsoft.com/en-us/libra ...

随机推荐

  1. amoeba

    Amoeba 原理:amoeba相当于业务员,处理client的读写请求,并将读写请求分开处理.amoeba和master以及slave都有联系,如果是读的请求,amoeba就从slave读取信息反馈 ...

  2. 2017年9月3日 Spring及Mybatis中连接数据库的不同方式

    连接数据库用spring和mybatis中使用的方法可以不同,mybaits可以不用写数据库的配置文件 Spring的连接方法 <!-- 读取属性文件(.properties)的内容 --> ...

  3. windows server 2012 + sql server 2008 r2安装

    windows server 2012 r2  里面安装 sql server 2008 r2 问题总结 前提是 windows server 2012 r2 已经安装完成  ,(仅仅是安装完成 啥服 ...

  4. 《Java从入门到放弃》JavaSE入门篇:面向对象语法二(入门版)

    想了半天,发现单独的封装和多态没什么好讲的,我们就简单说说Java里面对应的语法吧. 相关内容如下: 一.访问修饰符 二.getter/setter方法 三.构造方法 四.super和this 五.s ...

  5. 前端开发【第3篇:JavaScript序】

    JavaScript历史 聊聊JavaScript的诞生 JavaScirpt鼻祖:Bremdan Eich(布兰登·艾奇),JavaScript的诞生于浏览器的鼻祖网景公司(Netscape),发布 ...

  6. C语言中指针*p[N], (*P)[N],及**p的区别

    在C语言编程中指针经常困扰着我们,但是若能灵活运用指针的话,将会使得我们编程变得更加轻松与高效.这里讲下*p[N], (*P)[N],及**p的区别,这也是之前经常困扰我的地方. 这三者的定义分别为: ...

  7. IOS学习[Swift中跳转与传值]

    Swift中页面跳转与传值: 1.简单方式 首先,Swift的跳转可分为利用xib文件跳转与storyboard跳转两种方法,我这里选择使用storyboard的界面跳转方法. 1.通过在storyb ...

  8. 最近做了一个通达OA的大料:20170905最新版本破解可改单位名称,无限制安装

    最近做了一个通达OA的大料:20170905最新版本破解可改单位名称,无限制安装 用户约七十家,总体不错,修改了两次注册授权文件,完美使用中 可联系麦枫http://www.mfsun.com管理员Q ...

  9. 学号:201521123116 《java程序设计》第六周学习总结

    1. 本章学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 2.书面作业 1.1.Object对象中的clone方法是 ...

  10. 201521123050 《Java程序设计》第4周学习总结

    1. 本周学习总结 2. 书面作业 1.注释的应用 1.1使用类的注释与方法的注释为前面编写的类与方法进行注释,并在Eclipse中查看.(截图) 2.面向对象设计(大作业1,非常重要) 2.1 将在 ...