您当前的位置: 首页 >  后端

寒冰屋

暂无认证

  • 1浏览

    0关注

    2286博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

带有Upida/Jeneva.Net的ASP.NET MVC单页应用程序(后端)

寒冰屋 发布时间:2019-10-24 14:31:47 ,浏览量:1

目录

介绍

问题

问题1

问题2

问题3

解决方案

问题1——智能序列化

问题2——反向引用

问题3——映射更新

说明

  • 下载源3.4 MB
  • 在Codeplex上下载最新版本
  • 参见工作示例
介绍

让我们尝试使用最现代的技术创建一个简单的Web应用程序,看看我们可能会遇到什么问题。我将使用带有WebAPI的最新ASP.NET MVC和最新的NHibernate。请不要担心,所有技术也都适用于Entity Framework(可下载的ZIP档案也包含Entity Framework示例)。我将充分利用WebAPI的潜力——即浏览器和服务器之间的所有交互都将在JSON中异步进行。为了实现这一点,我将使用MVC JavaScript库——AngularJS。

请注意,本文仅关于后端。如果您对前端方面感到好奇,请访问此链接:带有Upida/Jeneva的ASP.NET MVC单页应用程序(前端/AngularJS)。

假设我们有一个包含两个表的简单数据库:Client 和Login,每个客户端可以有一对多的登录名。我的应用程序将包含三个页面——“客户端列表”,“创建客户端”和“编辑客户端”。“创建客户端”和“编辑客户端”页面将能够编辑客户端数据以及管理子登录列表。您可以在此处查看其工作方式。

首先,让我们定义域(或模型)类(映射在hbm文件中定义):

public class Client
{
    public virtual int? Id { get; set; }
    public virtual string Name { get; set; }
    public virtual string Lastname { get; set; }
    public virtual int? Age { get; set; }
    public virtual ISet Logins { get; set; }
} 

public class Login
{
    public virtual int? Id { get; set; }
    public virtual string Name { get; set; }
    public virtual string Password { get; set; }
    public virtual bool? Enabled { get; set; }
    public virtual Client Client { get; set; }
}

现在,我可以创建数据访问层。首先,我必须将其注入与基础DAO类NHibernate的 SessionFactory,并且定义了基本的DAO操作:Save,Delete,Update,Load,Get等。

public class Daobase : IDaobase
{
    public ISessionFactory SessionFactory { get; set; }

    public void Save(T entity)
    {
        this.SessionFactory
            .GetCurrentSession()
            .Save(entity);
    }
 
    public void Update(T entity)
    {
        this.SessionFactory
            .GetCurrentSession()
            .Update(entity);
    }
 
    public ITransaction BeginTransaction()
    {
        return this.SessionFactory
            .GetCurrentSession()
            .BeginTransaction();
    }
    /* others basic methods */
}

我只有一个DAO类-—— ClientDao。

public class ClientDao : Daobase, IClientDao
{
    public Client GetById(int id)
    {
        return this.SessionFactory
            .GetCurrentSession()
            .CreateQuery("from Client client left outer join fetch 
                          client.Logins where client.Id = :id")
            .SetParameter("id", id)
	        .SetResultTransformer(Transformers.DistinctRootEntity)
	        .UniqueResult();
    }

    public IList GetAll()
    {
	    return this.SessionFactory
            .GetCurrentSession()
            .CreateQuery("from Client client left outer join fetch client.Logins")
	        .SetResultTransformer(Transformers.DistinctRootEntity)
	        .List();
    }
}

DAO完成后,我们可以切换到服务层。服务通常负责打开和关闭事务。我只有一个服务类。它以受尊重的DAO类注入。

注意,Save和Update方法接受一个Client对象及其子级Logins,因此使用NHibernate级联(同时保留父级和子级)执行保存或更新。

public class ClientService : ICLientService
{
    public IClientDao ClientDao { get; set; }

    public Client GetById(int clientId)
    {
        Client item = this.ClientDao.GetById(clientId);
        return item;
    }
 
    public List GetAll()
    {
        List items = this.ClientDao.getAll();
        return items;
    }
 
    public void Save(Client item)
    {
        using(ITransaction tx = this.clientDao.BeginTransaction())
        {
            /* TODO: assign back-references of the child 
            Login objects - for each Login: item.Login[i].Client = item; */
            this.ClientDao.Save(item);
            tx.Commit();
        }
    }
 
    public void Update(Client item)
    {
        using(ITransaction tx = this.clientDao.BeginTransaction())
        {
            Client existing = this.clientDao.GetById(item.getId());
            /* TODO: copy changes from item to existing (recursively) */
            this.ClientDao.Merge(existing);
            tx.Commit();
        }
    }
}

让我们谈谈控制器。我将有两个控制器——一个用于HTML视图(MVC控制器),另一个用于JSON请求(API控制器)。它们都将被称为ClientController,但是将驻留在不同的命名空间中。MVC控制器将来自System.Web.Mvc.Controller,而API控制器将来自System.Web.Http.ApiController。MVC控制器将负责显示正确的视图。外观如下:

public class ClientController : System.Web.Mvc.Controller
{
    public ActionResult Index()
    {
        return this.View();
    }
 
    public ActionResult Create()
    {
        return this.View();
    }
 
    public ActionResult Edit()
    {
        return this.View();
    }
}

API控制器稍微复杂一点,因为它负责与数据库的交互。它被注入相应的服务层类。

public class ClientController : System.Web.Http.ApiController
{
    public ClientService ClientService { get; set;}
 
    public IList GetAll()
    {
        return this.ClientService.GetAll();
    }
 
    public Client GetById(int id)
    {
        return this.ClientService.GetById(id);
    }
 
    public void Save(Client item)
    {
        this.ClientService.Save(item);
    }
 
    public void Update(Client item)
    {
        this.ClientService.Update(item);
    }
}

现在,我们几乎拥有了所需的一切。MVC控制器将为我们提供HTML和JavaScript,它们将与API控制器进行异步交互并从数据库中获取数据。AngularJS将帮助我们将获取的数据显示为漂亮的HTML。我假设您熟悉AngularJS(或KnockoutJS),尽管本文并不那么重要。您唯一必须知道的是——每个页面都以静态HTML和JavaScript加载(没有任何服务器端脚本),在加载后,它与API控制器进行交互,以通过JSON从JSON异步加载数据库中所需的所有数据。而AngularJS有助于显示JSON作为美丽的HTML。

问题

现在,让我们谈谈当前实现中面临的问题。

问题1

第一个问题是序列化。从API控制器返回的数据被序列化为JSON。您可以在以下两种API控制器方法中看到它:

public class ClientController : System.Web.Http.ApiController
{ ....
    public IList GetAll()
    {
        return this.ClientService.GetAll();
    }
 
    public Client GetById(int id)
    {
        return this.ClientService.GetById(id);
    }

Client类是一个域类,它是包裹着NHibernate的包装器类。因此,对其进行序列化可能会导致循环依赖并导致StackOverflowException。但是还有其他一些小问题。例如,有时候我只需要Id和Name 字段可以在JSON中出现,有时候我需要所有的字段。当前的实现不允许我做出决定,它将始终序列化所有字段。

问题2

如果看一下ClientService类方法Save,您会发现缺少一些代码。

public void Save(Client item)
{
    using(ITransaction tx = this.clientDao.BeginTransaction())
    {
        /* code that assigns back-references of the child Login objects */
        this.ClientDao.Save(item);
        tx.Commit();
    }
}

这意味着,在保存Client对象之前,您必须设置子Login对象的反向引用。每个Login类都有一个字段——Client,它实际上是对父Client对象的反向引用。因此,为了节约Client与Logins一起使用级联保存,你必须建立这些领域的实际父实例。当Client从JSON反序列化,它不具有反向引用。在NHibernate用户中,这是一个众所周知的问题。

问题3

如果查看一下ClientService类方法Update,您将看到也缺少一些代码。

public void Update(Client item)
{
    using(ITransaction tx = this.ClientDao.OpenTransaction())
    {
        Client existing = this.ClientDao.GetById(item.getId());
        /* code that copies changes from item to existing */
        this.ClientDao.Merge(existing);
    }
}

我还必须实现将字段从反序列化的Client对象复制到相同的Client对象的现有持久性实例的逻辑。我的代码必须足够聪明才能通过子级Logins。它必须将现有的登录名与反序列化的登录名匹配,并分别复制字段。它还必须追加新添加的Logins,并删除丢失的内容。完成这些修改后,该Merge()方法将所有更改持久化到数据库。因此,这是相当复杂的逻辑。

在下一节中,我们将使用Jeneva.Net解决这三个问题。

解决方案 问题1——智能序列化

让我们看看Jeneva.Net如何帮助我们解决第一个问题。在ClientController中有两个方法返回Client的对象——GetAll()和GetById()。该GetAll()方法返回Clients的列表,该列表显示为网格。我不需要Client在JSON 中显示对象的所有字段。该GetById()方法在“编辑客户端”页面上使用。因此,此处需要完整的Client信息。

为了解决此问题,我必须遍历返回对象的每个属性,并将NULL值分配给不需要的每个属性。这似乎很艰苦,因为在每种方法上我都必须做不同的事情。Jeneva.Net为我们提供了可以为我们做的Jeneva.Mapper类。让我们使用Mapper类来修改服务层。

public class ClientService
{
    public IMapper Mapper { get; set; }
    public IClientDao ClientDao { get; set; }
 
    public Client GetById(int clientId)
    {
        Client item = this.ClientDao.GetById(clientId);
        return this.Mapper.Filter(item, Leves.DEEP);
    }
 
    public List GetAll()
    {
        List items = this.ClientDao.getAll();
        return this.Mapper.FilterList(items, Levels.GRID);
    }
 .....

它看起来非常简单,Mapper可以获取目标对象或对象列表并生成它们的副本,但是每个不需要的属性会被设置为NULL。第二个参数是代表序列化级别的数值。Jeneva.Net具有默认级别,但您可以自由定义自己的级别。

public class Levels
{
    public const byte ID = 10;
    public const byte LOOKUP = 20;
    public const byte GRID = 30;
    public const byte DEEP = 40;
    public const byte FULL = 50;
    public const byte NEVER = byte.MaxValue;
}

最后一步是用相应的级别装饰域类的每个属性。我将使用DtoAttribute中的Jeneva.Net装饰Client和Login类属性。

public class Client : Dtobase
{
    [Dto(Levels.ID)]
    public virtual int? Id { get; set; }
 
    [Dto(Levels.LOOKUP)]
    public virtual string Name { get; set; }
 
    [Dto(Levels.GRID)]
    public virtual string Lastname { get; set; }
 
    [Dto(Levels.GRID)]
    public virtual int? Age { get; set; }
 
    [Dto(Levels.GRID, Levels.LOOKUP)]
    public virtual ISet Logins { get; set; }
}
public class Login : Dtobase
{
    [Dto(Levels.ID)]
    public virtual int? Id { get; set; }
 
    [Dto(Levels.LOOKUP)]
    public virtual string Name { get; set; }
 
    [Dto(Levels.GRID)]
    public virtual string Password { get; set; }
 
    [Dto(Levels.GRID)]
    public virtual bool? Enabled { get; set; }
 
    [Dto(Levels.NEVER)]
    public virtual Client Client { get; set; }
}

装饰完所有属性后,可以使用Mapper类。例如,如果我使用Levels.ID调用Mapper.Filter()方法,那么将仅包含标记为ID的属性。如果我用Levels.LOOKUP调用Mapper.Filter()方法 ,则将包括标记为ID和LOOKUP的属性,因为ID小于LOOKUP(10

关注
打赏
1665926880
查看更多评论
0.0690s