在之前标题正文的基础上,添加
DDD套路,推荐“三部曲两头挤”:
public class Article : BaseEntity
{
public int? InCategoryId { get; set; }
public Category InCategory { get; set; }
public class Category : BaseEntity
{
//public IList<Article> Articles { get; set; } 可选
public string Name { get; set; }
<label>分类:</label> @Html.DropDownListFor(m=>m.SelectedCategoryId, new SelectList(Model.CategoryOptions,"Id", "Name"))
public class NewModel
{
public int SelectedCategoryId { get; set; }
public IList<CategoryModel> CategoryOptions { get; set; }
}
public class CategoryModel
{
public int Id { get; set; }
public string Name { get; set; }
public ActionResult New()
{
NewModel model = new NewModel
{
//为测试UI效果临时实现
CategoryOptions = new List<CategoryModel>
{
new CategoryModel { Id = 1, Name = "C#"}
}
};
return View(model);
之后引入Service(Mock)实现
CategoryOptions = service.GetCategories()
public class ArticleService : IArticleService
{
public IList<CategoryModel> GetCategories()
{
return new List<CategoryModel>
{
new CategoryModel { Id = 1, Name = "C#"},
new CategoryModel { Id = 2, Name = "SQL"}
ProdService的实现,依赖于:
public IList<CategoryModel> GetCategories()
{
IList<Category> categories = categoryRepository.GetBy(GetCurrentUser());
return mapper.Map<IList<CategoryModel>>(categories);
不要忘了BaseService中配置映射:
cfg.CreateMap<Category, VM.Article.CategoryModel>();注意这里只能使用Id进行比较:
public IList<Category> GetBy(User user)
{
return dbSet.Where(c => c.Owner.Id == user.Id).ToList();
演示:没有category选项
@想一想@:一开始用户没有分类怎么办?
我们选择实现方案1
if (categories.Count == 0) //ToList()之后的categories不会为null
{
Category defaultCategory = new Category { Name = "默认分类", Owner = current };
categoryRepository.Save(defaultCategory);
//不要忘了这一步,^_^
categories.Add(defaultCategory);
}//else nothing
在ArticleService的Publish()中带上分类。
现在可以使用AutoMapper了
Article article = mapper.Map<Article>(model);
Author无法自动映射,需要手动赋值:
article.Author = GetCurrentUser();
但注意Model和entity中属性名有差异,需要自定义映射:
cfg.CreateMap<NewModel, Article>()
.ForMember(
a => a.InCategoryId,
opt => opt.MapFrom(n => n.SelectedCategoryId))
复习:如果需要“纯面向对象”抛弃InCategoryId只保留InCategory的话,需要使用:
article.InCategory = categoryRepository.Load(model.SelectedCategoryId);
Keyword和Article多对多的entity声明和映射:略
View和Model:
@Html.TextBoxFor(m => m.Keywords)
public string Keywords { get; set; }
这里就出现了和entity中Article的Keywords不匹配的问题:
public class Article : BaseEntity
{
public List<Keyword> Keywords { get; set; }
用户输入的是带有空格的字符串,但我们后台需要的是entity集合。所以,在ArticleService.Publish()中:
//@想一想@:为什么要Trim()
string[] keywords = model.Keywords.Trim().Split(' ');
for (int i = 0; i < keywords.Length; i++)
{
//@想一想@:为什么要这个判断?
if (!string.IsNullOrWhiteSpace(keywords[i]))
{
Keyword keyword = new Keyword { Name = keywords[i] };
//复习:??运算符的作用
article.Keywords = article.Keywords ?? new List<Keyword>();
article.Keywords.Add(keyword);
不要忘了:用户输入的关键字有可能是重复的,这时候我们不能再new了哟!
所以需要利用KeywordRepository检查
public Keyword GetByName(string name)
{
return dbSet.Where(k => k.Name == name).SingleOrDefault();
但凡涉及到查询,就要想到索引!
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Keyword>()
//不然默认nvarchar(max)不能生成索引
.Property(k => k.Name).HasMaxLength(256);
modelBuilder.Entity<Keyword>()
.HasIndex(k => k.Name)
.IsUnique() //索引unique
最后的代码:
Keyword keyword = keywordRepository.GetByName(keywords[i]);
if (keyword == null)
{
keyword = new Keyword { Name = keywords[i] };
}
@想一想@:这样就OK了么?
标题和正文已经完成(略),关键在
比如:Author、Category和Keywords
所以首先要把这些关联对象显式的加载出来:
public Article GetWhole(int id) //命名太丑陋了,(*/ω\*)
{
return dbSet.Where(a => a.Id == id)
.Include(a => a.Author)
.Include(a => a.InCategory)
.Include(a => a.Keywords)
.SingleOrDefault();
相应的,在SingleModel中添加关联Model:
public class SingleModel
{
public _UserInfoModel Author { get; set; }
public CategoryModel InCategory { get; set; }
public IList<KeywordModel> Keywords { get; set; }
KeywordModel的映射需要AutoMapper配置:
cfg.CreateMap<Keyword, VM.Article.KeywordModel>();
最后,页面显示:
<a href="/User/@Model.Author.Id">@Model.Author.UserName</a>
<span>分类:</span> <a href="/Article/Category/@Model.InCategory.Id">@Model.InCategory.Name</a>
<span>关键字:</span>
@foreach (var item in Model.Keywords)
{
<a href="/Article/Keyword/@item.Id">@item.Name</a>
}
在文章单页添加一个编辑链接:/Article/Edit/{id}
@想一想@:文章Id从何而来?
<a href="/Article/Edit/@ViewContext.RouteData.Values["id"]">修改</a>
@想一想@:文章发布,和文章编辑,是否可共用:一个View/Model/Action?
除Action以外,其他都暂时是可以的,\(^o^)/
public ActionResult Edit(int id)
{
return View("New");
}
[HttpPost]
public ActionResult Edit(NewModel model, int id)
进入编辑页面,首先要加载被编辑文章内容:
NewModel model = service.GetEdit(id);
public NewModel GetEdit(int id)类似于SingleModel Get(int id),首先获取article(entity):
Article article = articleRepository.GetWhole(id);但是,
NewModel model = mapper.Map<NewModel>(article);所以不要忘了添加Map:
cfg.CreateMap<NewModel, Article>()
.ReverseMap() //简化代码
//但ignore的配置还是不能自动双向
.ForMember(n => n.Keywords, opt => opt.Ignore())
;
model.Keywords = string.Join(" ",
article.Keywords.Select(k=>k.Name).ToArray()
);
只能用所有POST内容“覆盖”原article内容,不管某项内容(比如Title/Category)用户实际上有无修改……@想一想@:为什么只能这样?
在Post的Action方法里面直接调用:
service.Edit(model, id);
对比之前的Publish()方法:
Article article = articleRepository.Find(id);
mapper.Map(model, article);
抽象之前的关键字转化代码,封装成方法transferKeywords(),然后调用:
transferKeywords(model.Keywords, article);
演示:抛出重复异常,
Violation of PRIMARY KEY constraint 'PK_dbo.KeywordArticles'
原因:复习多对多关系更改
解决方案,推荐引入LazyLoad,然后“先清场,后添加”:
article.Keywords.Clear();@想一想@:这样改了之后,transferKeywords()还能不能被Publish()和Edit()重用?
我们使用“静态消息”和“积分明细记录”机制。
因为使用(现阶段版本的)EF往集合中添加元素,会导致所有已有集合元素被加载,而User的Message和Credit可能会超级多,所以我们演示在Service层实现:
PS:以上不懂就复习:消息和积分的架构
Message message = new Message
{
Body = $"{user.UserName}使用了你的邀请码完成注册",
Receiver = invitedBy,
Kind = Kind.InvitedBy
};
messageRepository.Add(message);
Credit credit = new Credit
{
Owner = invitedBy,
Amount = 10,
Comment = $"{user.UserName}使用了你的邀请码完成注册",
Balance = creditRepository.GetLatest()?.Balance ?? 0 + 10
};
creditRepository.Add(credit);
注意:
protected UserRepository userRepository;
protected MessageRepository messageRepository;
protected CreditRepository creditRepository;
public BaseService()
{
userRepository = new UserRepository(currentDbContext);
public Credit GetLatest()
{
return dbSet.OrderByDescending(x => x.Id).FirstOrDefault();
演示:列表页
//能不能foreach,为什么?
for (int i = 0; i < Model.Items.Count; i++)
{
int fontWeight = Model.Items[i].ReadTime.HasValue ? 400 : 800;
<div style="font-weight:@fontWeight;" >
@*这个HiddenFor干嘛的?*@
@Html.HiddenFor(m => Model.Items[i].Id)
@Html.CheckBoxFor(m => Model.Items[i].Selected)
@Model.Items[i].Body
#体会#:利用fontWeight控制样式……
在POST的Action中:
public ActionResult Index(IndexModel model)
{
foreach (var item in model.Items)
{
if (item.Selected) //checkbox勾选
{
//item.Id:hidden input传回
messageService.Read(item.Id);
两种方案:
<button type="submit" formaction="/Message/Read">已读</button> <button type="submit" formaction="/Message/Delete">删除</button>对应:
public ActionResult Read(IndexModel model){
public ActionResult Delete(IndexModel model){
缺点:不利于代码重用
<input type="submit" name="submit" value="已读" /> <input type="submit" name="submit" value="删除" />在后台通过value区分究竟是哪一个按钮被点中:
string btnValue = Request.Form["submit"];于是就可以重用代码:
if (btnValue == "已读")//这种字符串的比较总是怪怪的,^_^
{
messageService.Read(item.Id);
}
else if (btnValue == "删除")
{
//因为“静态”消息,所以不用标记删除,就可以直接删除
messageService.Delete(item.Id);
}
else
{
//严谨做法:不是nothing #体会#
throw new Exception("前台传回非标准数据……");
}
另:Delete()的实现可以不用Find(),稍稍优化:(复习)
public void Delete(int it)
{
T entity = new T { Id = it };
dbSet.Attach(entity);
dbSet.Remove(entity);
为了给message的body套上HTML标签(听不懂还是复习,^_^)
在Global中添加HtmlTemplates文件夹,以及MessageTemplate,为“被作为邀请人注册”使用的方法Register.Invited()
namespace Global.HtmlTemplates
{
public static class MessageTemplate
{
public static class Register
{
public static string Invited(int id, string name)
{
return $@"<a href='/User/{id}'>{name}</a> 使用了你的邀请码完成注册";
其中,生成链接的功能还可以被进一步封装到SharedTemplate.Href:
public static string Common(string url, string text)
{
return $@"<a href='{url}'>{text}</a>";
所以,最终的代码:
string url = $"/User/{id}";
return $@"{SharedTemplate.Href.Common(url, name)}使用了你的邀请码完成注册";
在生成message的时候调用:
Body = Global.HtmlTemplates.MessageTemplate
.Register.Invited(user.Id, user.UserName),
复习:为什么需要?
我们可以直接创建一个控制台项目DbFactory,添加对
的引用。
我们首先需要拿到Repository中的DbContext,在Program.Main()中生成数据库和表结构:
SqlContext context = new SqlContext(); //因为LocalDB经常因为“被占用”无法删除,可以考虑总是手动删库 //context.Database.Delete(); context.Database.Create();
不要忘了在当前项目的App.config复制粘贴17bang的connectionStrings
准备好和Entity(或Controller)相对应的类,比如OnUser/OnArticle,每个类都有一个create()方法,里面依次生成一个一个的注册用户/文章等。
可以把生成并持久化entity的方法抽象出来,比如:
internal static void create()
{
fg = register("飞哥", null);
atai = register("atai", fg);
}
private static User register(string name, User invitedBy)
{
User user = new User
{
UserName = name,
Password = password,
InvitedBy = invitedBy
};
说明:
internal static User fg, atai;以供其他factory类使用,比如OnArticle时就需要作者
internal class OnArticle
{
internal static void create()
{
aspnet = publish("ASP.NET实战", "", OnUser.fg);
private static readonly string password = "1234";
专门建一个类:
internal class DbContextProvider {
private static readonly SqlContext context = new SqlContext();
internal static SqlContext GetContext()
{
return context;
确保所有的操作在同一个DbContext中完成!(复习:避免DbContext冲突)
DbContextProvider.GetContext().Users.Add(user); DbContextProvider.GetContext().SaveChanges(); //也可以:new UserRepository(DbContextProvider.GetContext()).Add(fg);
千万不要仅仅把DbFactory当做一个只存储entity本身的工具!
DbFactory中的数据应该是和entity的业务逻辑一致的,比如用户是通过Register生成的,Register()的过程中还:
一种解决方案是为User添加一个方法:
public Message GetMessageOnRegister()
{
return new Message
{
Body = Global.HtmlTemplates.MessageTemplate
.Register.Invited(Id, UserName),
Receiver = InvitedBy,
Kind = Kind.InvitedByr
然后,这个方法返回的Message就既可以在Service,也可以在DbFactory中使用!\(^o^)/
Message message = user.GetMessageOnRegister();
if (message.Receiver != null) //或者:if(message!=null)
{
DbContextProvider.GetContext().Messages.Add(message);
DbContextProvider.GetContext().SaveChanges();
演示:成功生成User和Message……
完成以下功能:
使用dbFactory创建数据,并完成:
多快好省!前端后端,线上线下,名师精讲
更多了解 加: