当前位置: 首页 > 工具软件 > Upida > 使用案例 >

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

万高轩
2023-12-01

目录

介绍

问题

问题1

问题2

问题3

解决方案

问题1——智能序列化

问题2——反向引用

问题3——映射更新

说明


介绍

让我们尝试使用最现代的技术创建一个简单的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<Login> 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; }
}

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

public class Daobase<T> : 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<Client>, 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<int>("id", id)
	        .SetResultTransformer(Transformers.DistinctRootEntity)
	        .UniqueResult<client>();
    }

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

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

注意SaveUpdate方法接受一个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<Client> GetAll()
    {
        List<Client> 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.ApiControllerMVC控制器将负责显示正确的视图。外观如下:

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

问题

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

问题1

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

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

Client类是一个域类,它是包裹着NHibernate包装器类。因此,对其进行序列化可能会导致循环依赖并导致StackOverflowException但是还有其他一些小问题。例如,有时候我只需要IdName 字段可以在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对象的反向引用。因此,为了节约ClientLogins一起使用级联保存,你必须建立这些领域的实际父实例。当ClientJSON反序列化,它不具有反向引用。在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的列表,该列表显示为网格。我不需要ClientJSON 中显示对象的所有字段。该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<Client> GetAll()
    {
        List<Client> 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装饰<code>ClientLogin类属性。

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<Login> 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()方法 ,则将包括标记为IDLOOKUP的属性,因为ID小于LOOKUP10 <20)。看一下该Client.Logins属性,您将看到在其中应用了两个级别,这意味着什么?这意味着,如果您使用Levels.GRID调用Mapper.Filter()方法,则将包括登录名,但是LOOKUP级别将应用于Login类的属性。而且,如果您调用的Mapper.Filter()级别高于GRID,则应用于Login属性的级别将分别更高。

问题2——反向引用

看一下服务层的类,Save方法。如您所见,此方法接受Client对象。我使用级联保存——我将Client及其Login一起保存。为了做到这一点,子级Login对象必须具有正确分配给父Client对象的反向引用。基本上,我必须遍历子级Logins并将Login.Client属性分配给根Client。完成此操作后,我可以使用NHibernate工具保存Client对象。

除了编写循环之外,我将再次使用Jeneva.Mapper类。让我们修改ClientService类。

public class ClientService
{
    public IMapper Mapper { get; set; }
    public IClientDao ClientDao { get; set; }
 ....
    public void Save(Client item)
    {
        using(ITransaction tx = this.ClientDao.BeginTransaction())
        {
            this.Mapper.Map(item);
            this.ClientDao.Save(item);
        }
    }

该代码将递归地遍历Client 对象的属性并设置所有反向引用。这实际上是解决方案的一半,另一半在此代码中。每个子类都必须实现IChild接口,在该接口中可以告知其父级是谁。该ConnectToParrent()方法将在内部由Mapper类调用。该Mapper会建议基于JSON可能父级。

public class Login : Dtobase, IChild
{
    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; }
 
    public void ConnectToParent(Object parent)
    {
        if(parent is Client)
        {
            this.Client = parent as Client;
        }
    }
}

如果正确实现了IChild接口,则只需从服务层调用Map()方法,所有反向引用都将被正确分配。

问题3——映射更新

第三个问题是最复杂的,因为更新客户端是一个复杂的过程。就我而言,我必须更新客户端字段以及更新子级登录名的字段,同时我必须追加,删除子级登录(如果用户已删除或插入新的登录名)。顺便说一句,即使您不使用级联更新,更新任何对象也很复杂。通常,因为要更新对象,总是必须编写自定义代码才能将更改从传入对象复制到现有对象。通常,传入对象仅包含几个要更新的重要字段,其余的是NULL,因此您不能依赖所有字段的盲目复制,因为您不希望将NULL复制到现有数据。

Mapper类可以将更改从传入对象复制到持久对象,而不覆盖任何重要字段。它是如何工作的?Jeneva.Net带有一个JenevaJsonFormatter类,该类派生自默认情况下ASP.NET MVC 5使用的Json.Net格式化程序。包含一些小的调整。如您所知,每个领域类都派生自Jeneva.Dtobase抽象类。此类包含属性名称HashSet。当UJenevaJsonFormatter解析JSON,它传递关于解析字段到Dtobase,而Dtobase对象记住哪些字段被分配。因此,每个领域对象都知道在JSON解析期间分配了哪些字段。之后,Mapper 类仅通过传入反序列化对象的分配属性,并将其值复制到现有的持久对象。

这是使用Mapper类的服务层Update()方法:

public class ClientService
{
    public IMapper Mapper { get; set; }
    public IClientDao ClientDao { get; set; }
 .... 
    public void Update(Client item)
    {
        using(ITransaction tx = this.ClientDao.OpenTransaction())
        {
            Client existing = this.ClientDao.load(item.getId());
            this.Mapper.MapTo(item, existing);
            this.ClientDao.Merge(existing);
        }
    }
}

这是Global.asax.cs,您可以看到如何在Web应用程序中将JenevaJsonFormatter设置为默认格式器。请不要担心从Json.Net格式化程序的切换。如果您看一下Jeneva格式化程序,它是从Json.Net派生而来,仅提供了微小的更改。在Application_Start事件中执行此代码。

GlobalConfiguration.Configuration.Formatters.Remove
(GlobalConfiguration.Configuration.Formatters.JsonFormatter);
GlobalConfiguration.Configuration.Formatters.Add(new JenevaJsonFormatter());

JenevaJsonFormatter还将所有的属性名称转换为Java约定:Name变为nameLoginName变为loginName。这些转换使JSON更具可移植性,并且对JavaScript更友好。它还使您无需进行任何修改即可用Java/Spring替换WebAPI后端。

说明

解决上述问题是Jeneva.Net 可以做到的最大方面。但是,还有另一个有趣的功能可以帮助您实现验证例程——服务器端和客户端。

您可以在本文中找到有关如何使用Jeneva.Net进行验证的更多详细信息:使用Upida.Net/Jeneva.Net验证传入的JSON。

另外,您还可以在我的下一篇文章中找到如何使用AngularJS创建单页Web应用程序(SPA):带有Upida/Jeneva的ASP.NET MVC单页应用程序(前端/AngularJS)

 类似资料: