第一部分 基本设计
目前最新版本的C#驱动MongoDB-CSharpDriver-2.2.3,比之前的版本更新比较大,在网上很难找到这个版本的相关C#操作资料,以下都是个人自发研究、测试的,如有雷同,不胜荣幸;如觉不妥,留言喷射;如有错误,还请赐教;如获帮助,示意欣赏。新版中有很多异步操作,本人对此没作研究,怕会产生数据安全问题,所以全部用的是同步方法。
1. 模型设计,使用GUID类型做为Id属性,在初始化时给一个随机值,基类代码,不作解释
using System;using System.Collections.Generic;public interface IEntityBase<T> { T Id { get; } } public abstract class AggregateBase:IEntityBase<Guid> { public Guid Id { get; private set; } public AggregateBase() { Id = Guid.NewGuid(); } }
2.设计MongoDb的数据上下文类 主要是几个静态方法 代码
using MongoDB.Driver; using System; namespace EFAndMongoRepostory { public class MongoDbContext { private static IMongoDatabase db; /// <summary> /// 设置并获取数据库 /// </summary> /// <param name="urls">连接地址</param> /// <param name="databaseName">数据库名称</param> /// <returns></returns> public static IMongoDatabase SetMongoDatabase(string urls,string databaseName) { MongoClient client = new MongoClient(urls); db = client.GetDatabase(databaseName); return client.GetDatabase(databaseName); } /// <summary> /// 获取数据库中的集合(相当于表) /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="name"></param> /// <returns></returns> public static IMongoCollection<TEntity> GetMongoCollection<TEntity>(string name) { IsDbNull();//检测数据库是否存在 return db.GetCollection<TEntity>(name); } private static void IsDbNull() { if (db != null) return; throw new Exception("the mongodb is null,plese set it on method SetMongoDatabase"); } /// <summary> /// 删除集合 /// </summary> /// <param name="name">集合名称</param> public static void DropCollection(string name) { db.DropCollection(name); } } }
3.Repostory类设计
1) 使用泛型约束,模拟事务控制,设计相关字段和方法。由于事务控制的是写入操作,我在这里借用MongoDB.Driver中的WriteModel<T>类,将所有写入操作记录在一个list集合中,只要没有发生回滚,并且list中有记录,就可以将所有的写入操作一次性提交,从而达到只与数据库交互一次的目的。具体字段作用看注释
using EFAndMongoRepostory.Entity; using MongoDB.Driver; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace EFAndMongoRepostory { public class MongoRepostory<TAggregate> where TAggregate :AggregateBase { /// <summary> /// 获取集合 /// </summary> protected IMongoCollection<TAggregate> Collection; /// <summary> /// 初始化,以类名作为集合名称 /// </summary> /// <param name="collection"></param> public MongoRepostory() { this.Collection = MongoDbContext.GetMongoCollection<TAggregate>(typeof(TAggregate).Name); } private List<WriteModel<TAggregate>> writers = new List<WriteModel<TAggregate>>();//写入模型集合 /// <summary> /// 指示是否起用事务,默认true /// </summary> public bool IsUseTransaction { get; set; } = true; private bool isRollback = false;//回滚控制 #region 事务控制 public void Commit() { )//如果不回滚,并且writers有数据 Collection.BulkWrite(writers); Rollback(); } public void Rollback() { writers.Clear();//清空writers } #endregion }
3)添加查询数据的方法,由于查询数据基本跟事务没什么关系,可以直接设计一些读数据的方法 代码
using EFAndMongoRepostory.Entity; using MongoDB.Driver; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace EFAndMongoRepostory { public class MongoRepostory<TAggregate> where TAggregate :AggregateBase { /// <summary> /// 获取集合 /// </summary> protected IMongoCollection<TAggregate> Collection; /// <summary> /// 初始化,以类名作为集合名称 /// </summary> /// <param name="collection"></param> public MongoRepostory() { this.Collection = MongoDbContext.GetMongoCollection<TAggregate>(typeof(TAggregate).Name); } private List<WriteModel<TAggregate>> writers = new List<WriteModel<TAggregate>>();//写入模型集合 /// <summary> /// 指示是否起用事务,默认true /// </summary> public bool IsUseTransaction { get; set; } = true; private bool isRollback = false;//回滚控制 #region 事务控制 public void Commit() { )//如果不回滚,并且writers有数据 Collection.BulkWrite(writers); Rollback(); } public void Rollback() { writers.Clear();//清空writers } #endregion #region 查询 /// <summary> /// 查找所有数据集合 /// </summary> /// <returns></returns> public IQueryable<TAggregate> FindAll() { return Collection.AsQueryable(); } /// <summary> /// 根据Id查找一条数据 /// </summary> /// <param name="id"></param> /// <returns></returns> public TAggregate FindById(Guid id) { var find = Collection.Find(o => o.Id == id); if (!find.Any()) return null; return find.FirstOrDefault(); } /// <summary> /// 根据过滤条件找出符合条件的集合 /// </summary> /// <param name="filter"></param> /// <returns></returns> public List<TAggregate> FindByFilter(Expression<Func<TAggregate, bool>> filter) { var find = Collection.Find(filter); if (!find.Any()) return null; return find.ToList(); } /// <summary> /// 根据过滤条件找出一条数据 /// </summary> /// <param name="filter"></param> /// <returns></returns> public TAggregate FindOne(Expression<Func<TAggregate, bool>> filter) { return Collection.Find(filter).FirstOrDefault(); } #endregion }
4)在每个写入操作的方法中,添加事务控制逻辑,包括是否启用事务逻辑,只要有一个环节发生错误,在提交时便发生回滚,所有操作无效,并不更改数据库中原有的数据。
这里特别提一下的是update操作,这被这个方法困扰了好长好长的时间,企图能在调用端不用引入想关的DLL,只需传入基本的参数就能完成。初步结论是我能力有限,对于lambd表达式还是不够熟,无奈之下,找到一个replace方法代替,发现可用,并且好用,唯一不爽的,你并不能用new()出来的数据作为参数传递,因为你一new,id值就会变,而mongodb中应该也有对于特殊字段名id不能更改的限制。其实再仔细想想逻辑,也应该是不能改变ID值的,因为ID代表着一条数据 的标识,你改变了对于这个数据来说就没有意义了。最后的解决方案是,第一,在方法注释中标注,添加关于这个问题的自定义异常信息。第二,在Update方法注释中,添加提示信息和示例,以防后期忘记相关mongodb的API,两个方法都能用就行。
具体流程控制在部分方法中作了详细注释,发现如果有什么不理解或异议,请留言,将在第一时间作出答复。完整Repostory代码
using EFAndMongoRepostory.Entity; using MongoDB.Driver; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace EFAndMongoRepostory { public class MongoRepostory<TAggregate> where TAggregate :AggregateBase { /// <summary> /// 获取集合 /// </summary> protected IMongoCollection<TAggregate> Collection; /// <summary> /// 初始化,以类名作为集合名称 /// </summary> /// <param name="collection"></param> public MongoRepostory() { this.Collection = MongoDbContext.GetMongoCollection<TAggregate>(typeof(TAggregate).Name); } private List<WriteModel<TAggregate>> writers = new List<WriteModel<TAggregate>>();//写入模型 /// <summary> /// 指示是否起用事务,默认true /// </summary> public bool IsUseTransaction { get; set; } = true; private bool isRollback = false;//回滚控制 #region 添加 /// <summary> /// 添加一条数据 /// </summary> /// <param name="entity"></param> public void Add(TAggregate entity) { if (entity == null) return; if (IsUseTransaction) { try { writers.Add(new InsertOneModel<TAggregate>(entity)); isRollback = false;//控制是否回滚 return; } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.InsertOne(entity); } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 添加数据集合 /// </summary> /// <param name="entities"></param> public void Add(IEnumerable<TAggregate> entities) { ) return; if(IsUseTransaction) { try { entities.ToList().ForEach(o => { writers.Add(new InsertOneModel<TAggregate>(o)); }); isRollback = false; return; } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.InsertMany(entities); } catch (Exception ex) { throw new Exception(ex.Message); } } #endregion #region 替换 /// <summary> /// 替换一条过滤的数据(请确保此方法Id属性是不能变) /// </summary> /// <param name="filter">过滤条件</param> /// <param name="enity">目标数据(目标数据的Id值必为源数据的Id)</param> public void ReplaceOne(Expression<Func<TAggregate, bool>> filter, TAggregate enity) { if (enity == null) return; if (IsUseTransaction) { try { writers.Add(new ReplaceOneModel<TAggregate>(Builders<TAggregate>.Filter.Where(filter), enity)); isRollback = false; return; } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.ReplaceOne(filter, enity); } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 替换一条数据(请确保此方法Id属性是不能变) /// </summary> /// <param name="id">目标id</param> /// <param name="enity">目标数据(目标数据的Id值必为源数据的Id)</param> public void ReplaceById(Guid id, TAggregate enity) { if (enity == null) return; if(enity.Id!=id) { isRollback = true; throw new Exception("the id can not change"); } if(IsUseTransaction) { try { writers.Add(new ReplaceOneModel<TAggregate>(Builders<TAggregate>.Filter.Eq(o=>o.Id, id), enity)); isRollback = false; return; } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.ReplaceOne(o => o.Id == id, enity); } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 查找一条数据并且替换 /// </summary> /// <param name="id">目标数据的id</param> /// <param name="enity">更改后的数据</param> /// <returns>更改前的数据</returns> public TAggregate FindOneAndReplace(Guid id, TAggregate enity) { if (enity == null) return null; if (enity.Id != id) { throw new Exception("the id can not change"); } return Collection.FindOneAndReplace(o => o.Id == id, enity); } /// <summary> /// 查找一条数据并且替换 /// </summary> /// <param name="filter">条件</param> /// <param name="enity">更改后的数据</param> /// <returns>更改前的数据</returns> public TAggregate FindOneAndReplace(Expression<Func<TAggregate,bool>>filter, TAggregate enity) { if (enity == null) return null; return Collection.FindOneAndReplace(filter, enity); } #endregion #region 移除 /// <summary> /// 根据过滤删除数据 /// </summary> /// <param name="filter"></param> public void RemoeMany(Expression<Func<TAggregate, bool>> filter) { if (IsUseTransaction) { try { writers.Add(new DeleteOneModel<TAggregate>(filter)); isRollback = false; return; } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.DeleteMany(filter); } catch (Exception ex) { throw new Exception(ex.Message); } } public void RemoveById(Guid id) { if (IsUseTransaction) { try { writers.Add(new DeleteOneModel<TAggregate>(Builders<TAggregate>.Filter.Eq(o => o.Id, id))); isRollback = false; return; } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.DeleteOne(o => o.Id == id); } catch (Exception ex) { throw new Exception(ex.Message); } } #endregion #region 更新 /// <summary> /// 过滤数据,执行更新操作(如不便使用,请用Replace相关的方法代替) /// /// 一般用replace来代替这个方法。其实这个功能还算强大的,可以很*修改多个属性 /// 关健是set参数比较不好配置,并且如果用此方法,调用端必须引用相关的DLL,set举例如下 /// set = Builders<TAggregate>.Update.Update.Set(o => o.Number, 1).Set(o => o.Description, "002.thml"); /// set作用:将指定TAggregate类型的实例对象的Number属性值更改为1,Description属性值改为"002.thml" /// 说明:Builders<TAggregate>.Update返回类型为UpdateDefinitionBuilder<TAggregate>,这个类有很多静态 /// 方法,Set()是其中一个,要求传入一个func的表达示,以指示当前要修改的,TAggregate类型中的属性类型, /// 另一个参数就是这个属性的值。 /// /// Builders<TAggregate>类有很多属性,返回很多如UpdateDefinitionBuilder<TAggregate>的很有用帮助类型 /// 可以能参CSharpDriver-2.2.3.chm文件 下载MongoDB-CSharpDriver时带有些文件 /// 或从官网https://docs.mongodb.com/ecosystem/drivers/csharp/看看 /// /// </summary> /// <param name="filter">过滤条件</param> /// <param name="set">修改设置</param> public void Update(Expression<Func<TAggregate, bool>> filter, UpdateDefinition<TAggregate> set) { if (set == null) return; if (IsUseTransaction)//如果启用事务 { try { writers.Add(new UpdateManyModel<TAggregate>(filter, set)); isRollback = false;//不回滚 return;//不执行后继操作 } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.UpdateMany(filter, set); } catch (Exception ex) { throw new Exception(ex.Message); } } /// <summary> /// 过滤数据,执行更新操作(如不便使用,请用Replace相关的方法代替) /// /// 一般用replace来代替这个方法。其实这个功能还算强大的,可以很*修改多个属性 /// 关健是set参数比较不好配置,并且如果用此方法,调用端必须引用相关的DLL,set举例如下 /// set = Builders<TAggregate>.Update.Update.Set(o => o.Number, 1).Set(o => o.Description, "002.thml"); /// set作用:将指定TAggregate类型的实例对象的Number属性值更改为1,Description属性值改为"002.thml" /// 说明:Builders<TAggregate>.Update返回类型为UpdateDefinitionBuilder<TAggregate>,这个类有很多静态 /// 方法,Set()是其中一个,要求传入一个func的表达示,以指示当前要修改的,TAggregate类型中的属性类型, /// 另一个参数就是这个属性的值。 /// /// Builders<TAggregate>类有很多属性,返回很多如UpdateDefinitionBuilder<TAggregate>的很有用帮助类型 /// 可以能参CSharpDriver-2.2.3.chm文件 下载MongoDB-CSharpDriver时带有些文件 /// 或从官网https://docs.mongodb.com/ecosystem/drivers/csharp/看看 /// /// </summary> /// <param name="id">找出指定的id数据</param> /// <param name="set">修改设置</param> public void Update(Guid id, UpdateDefinition<TAggregate> set) { if (set == null) return; if (IsUseTransaction)//如果启用事务 { try { writers.Add(new UpdateManyModel<TAggregate>(Builders<TAggregate>.Filter.Eq(o => o.Id, id), set)); isRollback = false;//不回滚 return;//不执行后继操作 } catch (Exception ex) { isRollback = true; throw new Exception(ex.Message); } } try { Collection.UpdateMany(o => o.Id == id, set); } catch (Exception ex) { throw new Exception(ex.Message); } } #endregion #region 事务控制 public void Commit() { )//如果不回滚,并且writers有数据 Collection.BulkWrite(writers); Rollback(); } public void Rollback() { writers.Clear();//清空writers } #endregion #region 查询 /// <summary> /// 查找所有数据集合 /// </summary> /// <returns></returns> public IQueryable<TAggregate> FindAll() { return Collection.AsQueryable(); } /// <summary> /// 根据Id查找一条数据 /// </summary> /// <param name="id"></param> /// <returns></returns> public TAggregate FindById(Guid id) { var find = Collection.Find(o => o.Id == id); if (!find.Any()) return null; return find.FirstOrDefault(); } /// <summary> /// 根据过滤条件找出符合条件的集合 /// </summary> /// <param name="filter"></param> /// <returns></returns> public List<TAggregate> FindByFilter(Expression<Func<TAggregate, bool>> filter) { var find = Collection.Find(filter); if (!find.Any()) return null; return find.ToList(); } /// <summary> /// 根据过滤条件找出一条数据 /// </summary> /// <param name="filter"></param> /// <returns></returns> public TAggregate FindOne(Expression<Func<TAggregate, bool>> filter) { return Collection.Find(filter).FirstOrDefault(); } #endregion /// <summary> /// 根据聚合类ID添加导航数据到 导航集合(中间表) /// </summary> /// <typeparam name="TNav">导航类</typeparam> /// <param name="nav">提供参数时直接new一个具体的nav类就行了</param> /// <param name="filter"></param> /// <param name="foreignKey"></param> public void AddByAggregate<TNav>(TNav nav, Expression<Func<TAggregate, bool>> filter, Guid foreignKey) where TNav : NavgationBase { //导航类的集合 var navCollection = MongoDbContext.GetMongoCollection<TNav>(typeof(TNav).Name); //遍历当前集合中所有符合条件的数据 Collection.Find(filter).ToList().ForEach(o => { //将导航类的属性赋相应的值 nav.AggregateId = foreignKey; nav.ValueObjectId = o.Id; //插入到数据库 navCollection.InsertOne(nav); }); } } }
5)截个图
6)Repostory类完成 是的,你可能在最后部分发现了一个额外的方法,这个方法是需要一个泛型参数,我理想的作用是用来配置集合(数据表)之关的多对多关系的,这也是仿EF中的通过一个中间表来实现,那么Model的设计就要改造一下了。另外我还有一个想法就是想设计专门针对聚合根中值类型的操作,但是这些还没有完成,时间太晚了,以后再慢慢搞起。可以看看现在model的设计,这里面没有分层,因我是在控制台下做测试的。另外添加了几个实现类,以备下次测试 ,没有注释,接口设计都还不完善,但是对事务部分的CURD没影响,现阶段model代码
using System; using System.Collections.Generic; namespace EFAndMongoRepostory.Entity { public interface IEntityBase<T> { T Id { get; } } public abstract class AggregateBase:IEntityBase<Guid> { public Guid Id { get; private set; } public AggregateBase() { Id = Guid.NewGuid(); } } public interface IValueObject { Guid Id { get; } } public abstract class ValueObjectBase { public Guid Id { get;} public ValueObjectBase() { Id = Guid.NewGuid(); } } public abstract class NavgationBase { public virtual Guid AggregateId { get; set; } public virtual Guid ValueObjectId { get; set; } } public class User : AggregateBase { public string Name { get; set; } public string Pwd { get; set; } public int Number { get; set; } public ICollection<Role> Roles { get; set; } = new List<Role>(); public ICollection<Permission> Permissions { get; set; } = new List<Permission>(); } public class Role :AggregateBase, IValueObject { public int Number { get; set; } public Guid UserGId { get; set; } public string Name { get; set; } public string Description { get; set; } public ICollection<Permission> Permissions { get; set; } = new List<Permission>(); } public class Permission:ValueObjectBase { public Guid UserId { get; set; } public Guid RoleId { get; set; } public string Name { get; set; } public string Url { get; set; } public bool HavePermission { get; set; } = true; } public class User_Role : NavgationBase { } public class Role_Permission : NavgationBase { } public class User_Permission : NavgationBase { } }
4.客户端测试 这里用的是控制台,而没有写测试代码,感觉这样跟写单元测试也没多大的区别。当然了,还是应该写单元测试的,起码代码复用高。Repostory设计好了,调用就简单了,我已经做过了部分测试,所以代码都是注释状态,如果你也想做测试,一步步的释放注释试试。代码
using EFAndMongoRepostory; using EFAndMongoRepostory.Entity; using MongoDB.Driver; using System; using System.Collections.Generic; using System.Linq; namespace MongoTest { class Program { static void Main(string[] args) { #region 初始化 //MongoClient client = new MongoClient("mongodb://localhost:27017"); //mongo客户端 //var db = client.GetDatabase("MongoTest"); //数据库 var db = MongoDbContext.SetMongoDatabase("mongodb://localhost:27017", "MongoTest"); #endregion #region 准备数据 List<Role> rList = new List<Role> { new Role { Name="r001", Description="rd001" }, new Role { Name="r002",Description="rd002" }, new Role { Name="r003",Description="rd003" } }; List<User> uList = new List<User> { new User { Name=", Pwd="pwd001" }, new User { Name=", Pwd="pwd002" } , new User { Name=", Pwd="pwd003" } , new User { Name=", Pwd="pwd004" } }; List<Permission> pList = new List<Permission> { ", Url="001.html" }, ", Url="002.html" }, ", Url="003.html" }, ", Url="004.html" }, ", Url="005.html" } }; #endregion var repostory = new MongoRepostory<User>(); //插入几条测试数据 //repostory.Add(uList); //repostory.Commit(); //测试替换操作 //var user = repostory.FindOne(o => o.Name == "001"); //user.Pwd = "password001"; //repostory.ReplaceById(user.Id, user); //repostory.Commit(); //Console.WriteLine(user.Name + ":" + user.Pwd); //测试更新操作 //var set = Builders<User>.Update.Set(o => o.Pwd, "pwd001").Set(o => o.Name, "01111"); //repostory.Update(user.Id, set); //repostory.Commit(); //Console.WriteLine(user.Name + ":" + user.Pwd); //测试替换前后数据 //var user = repostory.FindOne(o => o.Name == "01111"); //user.Name = "001"; //user = repostory.FindOneAndReplace(user.Id, user); //var u2 = repostory.FindById(user.Id); //Console.WriteLine(user.Name + ":" + user.Pwd); //Console.WriteLine("替换后的"); //Console.WriteLine(u2.Name + ":" + u2.Pwd); var query = repostory.FindAll().ToList(); query.ForEach(o => { Console.WriteLine(o.Name + ":" + o.Pwd); }); Console.ReadKey(); } } }
5.关于门外汉和OADemo 之所以说是门外汉,是因为我没有从事过软件开发工作,都是业余时间学习的。而正是基于此,之前写的OADemo我也觉得应该放了,毕竟没做过真实项目,现在园子里有一个很火的关于mvc的精彩连续剧 ,专业,详细。如果有关注我之前的OADemo可以移步关注。我写这个的目的除了很少一部分是备忘,更大一部分是来自虚荣心,我也希望能得到关注,肯定。其实本来是想在Domain层完成权限块的,也不知怎么的就关注到MongoDB这来了,还真是不专心啊。
打住了,再发到首页去看看,能不能通过,已经两篇心血被下架了,还过不了就严重受打击了。