diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/EmailLib/EmailConfiguration.cs b/EmailLib/EmailConfiguration.cs new file mode 100644 index 0000000..5d5c669 --- /dev/null +++ b/EmailLib/EmailConfiguration.cs @@ -0,0 +1,12 @@ +namespace TechHelper.Features +{ + public static class EmailConfiguration + { + public static string EmailFrom { get; set; } = "1468441589@qq.com"; + public static string Password { get; set; } = "pfxhtoztjimtbahc"; + public static string SubDescribe { get; set; } = "这是你的验证凭证"; + public static string SmtpHost { get; set; } = "smtp.qq.com"; + public static int SmtpPort { get; set; } = 587; + + } +} diff --git a/EmailLib/EmailLib.csproj b/EmailLib/EmailLib.csproj new file mode 100644 index 0000000..c1c0809 --- /dev/null +++ b/EmailLib/EmailLib.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/EmailLib/EmailTemap.cs b/EmailLib/EmailTemap.cs new file mode 100644 index 0000000..e442cf6 --- /dev/null +++ b/EmailLib/EmailTemap.cs @@ -0,0 +1,109 @@ +namespace Email +{ + public class EmailTemap + { + public static string _email = @" + + + + + {{AppName}} - Verification Code + + + +
+
+

您的验证码

+
+

您好,

+
+

您收到这封邮件是因为您请求验证您在 {{AppName}} 的邮箱地址。

+

请使用以下验证码完成操作:

+
+
+ {{VerificationCode}} +
+

请在应用程序或网站中输入此验证码。

+

为了您的账户安全,请勿将此验证码透露给任何人。

+

此验证码在 {{ExpirationMinutes}} 分钟内有效。

+

如果您没有进行此操作,请忽略本邮件。

+
+

这是一封自动发送的邮件,请勿直接回复。

+

© {{AppName}}

+

支持: Tech Helper

+
+
+ +"; + + + + + + } +} \ No newline at end of file diff --git a/EmailLib/IEmailSender.cs b/EmailLib/IEmailSender.cs new file mode 100644 index 0000000..c4674b3 --- /dev/null +++ b/EmailLib/IEmailSender.cs @@ -0,0 +1,18 @@ +namespace TechHelper.Features +{ + public interface IEmailSender + { + /// + /// 使用 MailKit 通过邮箱发送邮件 + /// + /// 收件人邮箱地址 + /// 邮件主题 + /// 邮件正文 (支持 HTML) + /// + public Task SendEmailAsync(string toEmail,string subject, string body); + + public Task SendEmailAsync(string toEmail, string body); + + public Task SendEmailAuthcodeAsync(string toEmail, string authCode); + } +} diff --git a/EmailLib/QEmailSender.cs b/EmailLib/QEmailSender.cs new file mode 100644 index 0000000..d3e7b46 --- /dev/null +++ b/EmailLib/QEmailSender.cs @@ -0,0 +1,79 @@ +using MailKit.Net.Smtp; +using MailKit.Security; +using MimeKit; +using Email; +using System.Text; + +namespace TechHelper.Features +{ + public class QEmailSender : IEmailSender + { + public async Task SendEmailAsync(string toEmail, string subject, string body) + { + var secureSocketOption = SecureSocketOptions.StartTls; + try + { + var message = new MimeMessage(); + message.From.Add(new MailboxAddress("TechHelper", EmailConfiguration.EmailFrom)); + message.To.Add(new MailboxAddress("", toEmail)); + message.Subject = subject; + message.Body = new TextPart(MimeKit.Text.TextFormat.Html) + { + Text = body + }; + + + using (var client = new SmtpClient()) + { + await client.ConnectAsync(EmailConfiguration.SmtpHost, EmailConfiguration.SmtpPort, secureSocketOption); + + // 移除 XOAUTH2 认证机制,避免某些环境下出现异常 + // 如果不加这行,某些情况下可能会遇到 MailKit.Security.AuthenticationException: XOAUTH2 is not supported + client.AuthenticationMechanisms.Remove("XOAUTH2"); + + await client.AuthenticateAsync(EmailConfiguration.EmailFrom, EmailConfiguration.Password); + await client.SendAsync(message); + await client.DisconnectAsync(true); + } + + Console.WriteLine("邮件发送成功 (QQ 邮箱)!"); + } + catch (TypeInitializationException ex) + { + Console.WriteLine("捕获到 SmtpClient 的 TypeInitializationException!"); + Console.WriteLine($"异常消息: {ex.Message}"); + + Exception? inner = ex.InnerException; // 获取第一层内部异常 + int innerCount = 1; + while (inner != null) + { + Console.WriteLine($"--> 内部异常 {innerCount}: {inner.GetType().Name}"); + Console.WriteLine($"--> 内部异常消息: {inner.Message}"); + inner = inner.InnerException; // 获取下一层内部异常 + innerCount++; + } + } + catch (Exception ex) + { + Console.WriteLine($"捕获到其他类型的异常: {ex.GetType().Name}"); + Console.WriteLine($"异常消息: {ex.Message}"); + } + } + + public async Task SendEmailAsync(string toEmail , string body) + { + await SendEmailAsync(toEmail, EmailConfiguration.SubDescribe, body); + } + + public async Task SendEmailAuthcodeAsync(string toEmail, string authCode) + { + string htmlTemplateString = EmailTemap._email; + string htmlBody = htmlTemplateString + .Replace("{{AppName}}", "TechHelper") + .Replace("{{VerificationCode}}", authCode) + .Replace("{{ExpirationMinutes}}", "30") + .Replace("{{SupportEmail}}", "TechHelper"); + await SendEmailAsync(toEmail, htmlBody); + } + } +} diff --git a/Entities/Configuration/ApiConfiguration.cs b/Entities/Configuration/ApiConfiguration.cs new file mode 100644 index 0000000..9f9582a --- /dev/null +++ b/Entities/Configuration/ApiConfiguration.cs @@ -0,0 +1,7 @@ +namespace Entities.Configuration +{ + public class ApiConfiguration + { + public string? BaseAddress { get; set; } /*= "http://localhost:5099";*/ + } +} diff --git a/Entities/Configuration/JwtConfiguration.cs b/Entities/Configuration/JwtConfiguration.cs new file mode 100644 index 0000000..86b6ad1 --- /dev/null +++ b/Entities/Configuration/JwtConfiguration.cs @@ -0,0 +1,10 @@ +namespace Entities.Configuration +{ + public class JwtConfiguration + { + public string? SecurityKey { get; set; } + public string? ValidIssuer { get; set; } + public string? ValidAudience { get; set; } + public int ExpiryInMinutes { get; set; } + } +} diff --git a/Entities/Context/IPagedList.cs b/Entities/Context/IPagedList.cs new file mode 100644 index 0000000..1318bfe --- /dev/null +++ b/Entities/Context/IPagedList.cs @@ -0,0 +1,57 @@ +namespace SharedDATA.Context +{ + + using System.Collections.Generic; + /// + /// Provides the interface(s) for paged list of any type. + /// 为任何类型的分页列表提供接口 + /// + /// The type for paging.分页的类型 + public interface IPagedList + { + /// + /// Gets the index start value. + /// 获取索引起始值 + /// + /// The index start value. + int IndexFrom { get; } + /// + /// Gets the page index (current). + /// 获取页索引(当前) + /// + int PageIndex { get; } + /// + /// Gets the page size. + /// 获取页面大小 + /// + int PageSize { get; } + /// + /// Gets the total count of the list of type + /// 获取类型列表的总计数 + /// + int TotalCount { get; } + /// + /// Gets the total pages. + /// 获取页面总数 + /// + int TotalPages { get; } + /// + /// Gets the current page items. + /// 获取当前页项 + /// + IList Items { get; } + /// + /// Gets the has previous page. + /// 获取前一页 + /// + /// The has previous page. + bool HasPreviousPage { get; } + + /// + /// Gets the has next page. + /// 获取下一页 + /// + /// The has next page. + bool HasNextPage { get; } + } +} diff --git a/Entities/Context/PagedList.cs b/Entities/Context/PagedList.cs new file mode 100644 index 0000000..fe70f31 --- /dev/null +++ b/Entities/Context/PagedList.cs @@ -0,0 +1,238 @@ +namespace SharedDATA.Context +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Represents the default implementation of the interface. + /// + /// The type of the data to page 页类型的数据 + public class PagedList : IPagedList + { + /// + /// Gets or sets the index of the page. + /// 获得页的起始页 + /// + /// The index of the page. + public int PageIndex { get; set; } + /// + /// Gets or sets the size of the page. + /// 获得页大小 + /// + /// The size of the page. + public int PageSize { get; set; } + /// + /// Gets or sets the total count. + /// 获得总数 + /// + /// The total count. + public int TotalCount { get; set; } + /// + /// Gets or sets the total pages. + /// 获得总页数 + /// + /// The total pages. + public int TotalPages { get; set; } + /// + /// Gets or sets the index from. + /// 从索引起 + /// + /// The index from. + public int IndexFrom { get; set; } + + /// + /// Gets or sets the items. + /// 数据 + /// + /// The items. + public IList Items { get; set; } + + /// + /// Gets the has previous page. + /// 获取前一页 + /// + /// The has previous page. + public bool HasPreviousPage => PageIndex - IndexFrom > 0; + + /// + /// Gets the has next page. + /// 获取下一页 + /// + /// The has next page. + public bool HasNextPage => PageIndex - IndexFrom + 1 < TotalPages; + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// The index of the page. + /// The size of the page. + /// The index from. + public PagedList(IEnumerable source, int pageIndex, int pageSize, int indexFrom) + { + if (indexFrom > pageIndex) + { + throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex"); + } + + if (source is IQueryable querable) + { + PageIndex = pageIndex; + PageSize = pageSize; + IndexFrom = indexFrom; + TotalCount = querable.Count(); + TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize); + + Items = querable.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToList(); + } + else + { + PageIndex = pageIndex; + PageSize = pageSize; + IndexFrom = indexFrom; + TotalCount = source.Count(); + TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize); + + Items = source.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToList(); + } + } + + /// + /// Initializes a new instance of the class. + /// + public PagedList() => Items = new T[0]; + } + + + /// + /// Provides the implementation of the and converter. + /// + /// The type of the source. + /// The type of the result. + public class PagedList : IPagedList + { + /// + /// Gets the index of the page. + /// + /// The index of the page. + public int PageIndex { get; } + /// + /// Gets the size of the page. + /// + /// The size of the page. + public int PageSize { get; } + /// + /// Gets the total count. + /// + /// The total count. + public int TotalCount { get; } + /// + /// Gets the total pages. + /// + /// The total pages. + public int TotalPages { get; } + /// + /// Gets the index from. + /// + /// The index from. + public int IndexFrom { get; } + + /// + /// Gets the items. + /// + /// The items. + public IList Items { get; } + + /// + /// Gets the has previous page. + /// + /// The has previous page. + public bool HasPreviousPage => PageIndex - IndexFrom > 0; + + /// + /// Gets the has next page. + /// + /// The has next page. + public bool HasNextPage => PageIndex - IndexFrom + 1 < TotalPages; + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// The converter. + /// The index of the page. + /// The size of the page. + /// The index from. + public PagedList(IEnumerable source, Func, IEnumerable> converter, int pageIndex, int pageSize, int indexFrom) + { + if (indexFrom > pageIndex) + { + throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex"); + } + + if (source is IQueryable querable) + { + PageIndex = pageIndex; + PageSize = pageSize; + IndexFrom = indexFrom; + TotalCount = querable.Count(); + TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize); + + var items = querable.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToArray(); + + Items = new List(converter(items)); + } + else + { + PageIndex = pageIndex; + PageSize = pageSize; + IndexFrom = indexFrom; + TotalCount = source.Count(); + TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize); + + var items = source.Skip((PageIndex - IndexFrom) * PageSize).Take(PageSize).ToArray(); + + Items = new List(converter(items)); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// The source. + /// The converter. + public PagedList(IPagedList source, Func, IEnumerable> converter) + { + PageIndex = source.PageIndex; + PageSize = source.PageSize; + IndexFrom = source.IndexFrom; + TotalCount = source.TotalCount; + TotalPages = source.TotalPages; + + Items = new List(converter(source.Items)); + } + } + + /// + /// Provides some help methods for interface. + /// + public static class PagedList + { + /// + /// Creates an empty of . + /// + /// The type for paging + /// An empty instance of . + public static IPagedList Empty() => new PagedList(); + /// + /// Creates a new instance of from source of instance. + /// + /// The type of the result. + /// The type of the source. + /// The source. + /// The converter. + /// An instance of . + public static IPagedList From(IPagedList source, Func, IEnumerable> converter) => new PagedList(source, converter); + } +} diff --git a/Entities/Context/UnitOfWork/IEnumerablePagedListExtensions.cs b/Entities/Context/UnitOfWork/IEnumerablePagedListExtensions.cs new file mode 100644 index 0000000..a19848e --- /dev/null +++ b/Entities/Context/UnitOfWork/IEnumerablePagedListExtensions.cs @@ -0,0 +1,39 @@ +namespace SharedDATA.Api +{ + using SharedDATA.Context; + using System; + using System.Collections.Generic; + + /// + /// Provides some extension methods for to provide paging capability. + /// 提供一些扩展方法来提供分页功能 + /// + public static class IEnumerablePagedListExtensions + { + /// + /// Converts the specified source to by the specified and . + /// 通过起始页和页大小把数据转换成页集合 + /// + /// The type of the source.源的类型 + /// The source to paging.分页的源 + /// The index of the page.起始页 + /// The size of the page.页大小 + /// The start index value.开始索引值 + /// An instance of the inherited from interface.接口继承的实例 + public static IPagedList ToPagedList(this IEnumerable source, int pageIndex, int pageSize, int indexFrom = 0) => new PagedList(source, pageIndex, pageSize, indexFrom); + + /// + /// Converts the specified source to by the specified , and + /// 通过转换器,起始页和页大小把数据转换成页集合 + /// + /// The type of the source.源的类型 + /// The type of the result.反馈的类型 + /// The source to convert.要转换的源 + /// The converter to change the to .转换器来改变源到反馈 + /// The page index.起始页 + /// The page size.页大小 + /// The start index value.开始索引值 + /// An instance of the inherited from interface.接口继承的实例 + public static IPagedList ToPagedList(this IEnumerable source, Func, IEnumerable> converter, int pageIndex, int pageSize, int indexFrom = 0) => new PagedList(source, converter, pageIndex, pageSize, indexFrom); + } +} diff --git a/Entities/Context/UnitOfWork/IQueryablePageListExtensions.cs b/Entities/Context/UnitOfWork/IQueryablePageListExtensions.cs new file mode 100644 index 0000000..046589e --- /dev/null +++ b/Entities/Context/UnitOfWork/IQueryablePageListExtensions.cs @@ -0,0 +1,52 @@ +namespace SharedDATA.Api +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using SharedDATA.Context; + + public static class IQueryablePageListExtensions + { + /// + /// Converts the specified source to by the specified and . + /// 根据起始页和页大小转换成源 + /// + /// The type of the source.源的类型 + /// The source to paging.分页的源 + /// The index of the page.起始页 + /// The size of the page.页大小 + /// + /// A to observe while waiting for the task to complete. + /// 在等待任务完成时观察 + /// + /// The start index value.值的起始索引 + /// An instance of the inherited from interface.接口继承的实例 + public static async Task> ToPagedListAsync(this IQueryable source, int pageIndex, int pageSize, int indexFrom = 0, CancellationToken cancellationToken = default(CancellationToken)) + { + //如果索引比起始页大,则异常 + if (indexFrom > pageIndex) + { + throw new ArgumentException($"indexFrom: {indexFrom} > pageIndex: {pageIndex}, must indexFrom <= pageIndex"); + } + //数据源大小 + var count = await source.CountAsync(cancellationToken).ConfigureAwait(false); + + var items = await source.Skip((pageIndex - indexFrom) * pageSize) + .Take(pageSize).ToListAsync(cancellationToken).ConfigureAwait(false); + + var pagedList = new PagedList() + { + PageIndex = pageIndex, + PageSize = pageSize, + IndexFrom = indexFrom, + TotalCount = count, + Items = items, + TotalPages = (int)Math.Ceiling(count / (double)pageSize) + }; + + return pagedList; + } + } +} diff --git a/Entities/Context/UnitOfWork/IRepository.cs b/Entities/Context/UnitOfWork/IRepository.cs new file mode 100644 index 0000000..b397860 --- /dev/null +++ b/Entities/Context/UnitOfWork/IRepository.cs @@ -0,0 +1,463 @@ +namespace SharedDATA.Api +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore.Query; + using Microsoft.EntityFrameworkCore.ChangeTracking; + using Microsoft.EntityFrameworkCore; + using SharedDATA.Context; + + /// + /// Defines the interfaces for generic repository. + /// 为通用存储库定义接口 + /// + /// The type of the entity.实体类型 + public interface IRepository where TEntity : class + { + /// + /// Changes the table name. This require the tables in the same database. + /// 更改表名。这需要相同数据库中的表 + /// + /// + /// + /// This only been used for supporting multiple tables in the same model. This require the tables in the same database. + /// 这只用于支持同一个模型中的多个表。这需要相同数据库中的表。 + /// + void ChangeTable(string table); + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// 基于谓词、orderby委托和页面信息获取。此方法默认无跟踪查询。 + /// + /// A function to test each element for a condition.用于测试条件的每个元素的函数 + /// A function to order elements.对元素进行排序的函数 + /// A function to include navigation properties 包含导航属性的函数 + /// The index of page.起始页 + /// The size of the page.页大小 + /// True to disable changing tracking; otherwise, 禁用更改跟踪false. Default to true. + /// Ignore query filters 忽略查询过滤器 + /// An that contains elements that satisfy the condition specified by 包含满足指定条件的元素. + /// This method default no-tracking query. + IPagedList GetPagedList(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// 基于谓词、orderby委托和页面信息获取。此方法默认无跟踪查询。 + /// + /// A function to test each element for a condition.用于测试条件的每个元素的函数 + /// A function to order elements.对元素进行排序的函数 + /// A function to include navigation properties 包含导航属性的函数 + /// The index of page.起始页 + /// The size of the page.页大小 + /// True to disable changing tracking;禁用更改跟踪; otherwise, false. Default to true. + /// + /// A to observe while waiting for the task to complete. + /// + /// Ignore query filters 忽略查询过滤器 + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query.此方法默认无跟踪查询 + Task> GetPagedListAsync(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + CancellationToken cancellationToken = default(CancellationToken), + bool ignoreQueryFilters = false); + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// The index of page. + /// The size of the page. + /// True to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + IPagedList GetPagedList(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + bool ignoreQueryFilters = false) where TResult : class; + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// The index of page. + /// The size of the page. + /// True to disable changing tracking; otherwise, false. Default to true. + /// + /// A to observe while waiting for the task to complete. + /// + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + Task> GetPagedListAsync(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + CancellationToken cancellationToken = default(CancellationToken), + bool ignoreQueryFilters = false) where TResult : class; + + /// + /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query. + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method defaults to a read-only, no-tracking query. + TEntity GetFirstOrDefault(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method defaults to a read-only, no-tracking query. + TResult GetFirstOrDefault(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// Ex: This method defaults to a read-only, no-tracking query. + Task GetFirstOrDefaultAsync(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method defaults to a read-only, no-tracking query. + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// Ex: This method defaults to a read-only, no-tracking query. + Task GetFirstOrDefaultAsync(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Uses raw SQL queries to fetch the specified data. + /// + /// The raw SQL. + /// The parameters. + /// An that contains elements that satisfy the condition specified by raw SQL. + IQueryable FromSql(string sql, params object[] parameters); + + /// + /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned. + /// + /// The values of the primary key for the entity to be found. + /// The found entity or null. + TEntity Find(params object[] keyValues); + + /// + /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned. + /// + /// The values of the primary key for the entity to be found. + /// A that represents the asynchronous find operation. The task result contains the found entity or null. + ValueTask FindAsync(params object[] keyValues); + + /// + /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned. + /// + /// The values of the primary key for the entity to be found. + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous find operation. The task result contains the found entity or null. + ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken); + + /// + /// Gets all entities. This method is not recommended + /// + /// The . + IQueryable GetAll(); + + /// + /// Gets all entities. This method is not recommended + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// Ex: This method defaults to a read-only, no-tracking query. + IQueryable GetAll(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Gets all entities. This method is not recommended + /// + /// The . + Task> GetAllAsync(); + + /// + /// Gets all entities. This method is not recommended + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// Ex: This method defaults to a read-only, no-tracking query. + Task> GetAllAsync(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false); + + /// + /// Gets the count based on a predicate. + /// + /// + /// + int Count(Expression> predicate = null); + + /// + /// Gets async the count based on a predicate. + /// + /// + /// + Task CountAsync(Expression> predicate = null); + + /// + /// Gets the long count based on a predicate. + /// + /// + /// + long LongCount(Expression> predicate = null); + + /// + /// Gets async the long count based on a predicate. + /// + /// + /// + Task LongCountAsync(Expression> predicate = null); + + /// + /// Gets the max based on a predicate. + /// + /// + /// /// + /// decimal + T Max(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the async max based on a predicate. + /// + /// + /// /// + /// decimal + Task MaxAsync(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the min based on a predicate. + /// + /// + /// + /// decimal + T Min(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the async min based on a predicate. + /// + /// + /// + /// decimal + Task MinAsync(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the average based on a predicate. + /// + /// + /// /// + /// decimal + decimal Average(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the async average based on a predicate. + /// + /// + /// /// + /// decimal + Task AverageAsync(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the sum based on a predicate. + /// + /// + /// /// + /// decimal + decimal Sum(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the async sum based on a predicate. + /// + /// + /// /// + /// decimal + Task SumAsync(Expression> predicate = null, Expression> selector = null); + + /// + /// Gets the Exists record based on a predicate. + /// + /// + /// + bool Exists(Expression> selector = null); + /// + /// Gets the Async Exists record based on a predicate. + /// + /// + /// + Task ExistsAsync(Expression> selector = null); + + /// + /// Inserts a new entity synchronously. + /// + /// The entity to insert. + TEntity Insert(TEntity entity); + + /// + /// Inserts a range of entities synchronously. + /// + /// The entities to insert. + void Insert(params TEntity[] entities); + + /// + /// Inserts a range of entities synchronously. + /// + /// The entities to insert. + void Insert(IEnumerable entities); + + /// + /// Inserts a new entity asynchronously. + /// + /// The entity to insert. + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous insert operation. + ValueTask> InsertAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Inserts a range of entities asynchronously. + /// + /// The entities to insert. + /// A that represents the asynchronous insert operation. + Task InsertAsync(params TEntity[] entities); + + /// + /// Inserts a range of entities asynchronously. + /// + /// The entities to insert. + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous insert operation. + Task InsertAsync(IEnumerable entities, CancellationToken cancellationToken = default(CancellationToken)); + + /// + /// Updates the specified entity. + /// + /// The entity. + void Update(TEntity entity); + + /// + /// Updates the specified entities. + /// + /// The entities. + void Update(params TEntity[] entities); + + /// + /// Updates the specified entities. + /// + /// The entities. + void Update(IEnumerable entities); + + /// + /// Deletes the entity by the specified primary key. + /// + /// The primary key value. + void Delete(object id); + + /// + /// Deletes the specified entity. + /// + /// The entity to delete. + void Delete(TEntity entity); + + /// + /// Deletes the specified entities. + /// + /// The entities. + void Delete(params TEntity[] entities); + + /// + /// Deletes the specified entities. + /// + /// The entities. + void Delete(IEnumerable entities); + + /// + /// Change entity state for patch method on web api. + /// + /// The entity. + /// /// The entity state. + void ChangeEntityState(TEntity entity, EntityState state); + } +} diff --git a/Entities/Context/UnitOfWork/IRepositoryFactory.cs b/Entities/Context/UnitOfWork/IRepositoryFactory.cs new file mode 100644 index 0000000..127e67e --- /dev/null +++ b/Entities/Context/UnitOfWork/IRepositoryFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace SharedDATA.Api +{ + /// + /// Defines the interfaces for interfaces. + /// + public interface IRepositoryFactory + { + /// + /// Gets the specified repository for the . + /// + /// True if providing custom repositry + /// The type of the entity. + /// An instance of type inherited from interface. + IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class; + } +} diff --git a/Entities/Context/UnitOfWork/IUnitOfWork.cs b/Entities/Context/UnitOfWork/IUnitOfWork.cs new file mode 100644 index 0000000..35293f6 --- /dev/null +++ b/Entities/Context/UnitOfWork/IUnitOfWork.cs @@ -0,0 +1,78 @@ + + +namespace SharedDATA.Api +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.ChangeTracking; + + /// + /// Defines the interface(s) for unit of work. + /// + public interface IUnitOfWork : IDisposable + { + + /// + /// Changes the database name. This require the databases in the same machine. NOTE: This only work for MySQL right now. + /// + /// The database name. + /// + /// This only been used for supporting multiple databases in the same model. This require the databases in the same machine. + /// + void ChangeDatabase(string database); + + /// + /// Gets the specified repository for the . + /// + /// True if providing custom repositry + /// The type of the entity. + /// An instance of type inherited from interface. + IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class; + + /// + /// Gets the db context. + /// + /// + TContext GetDbContext() where TContext : DbContext; + + /// + /// Saves all changes made in this context to the database. + /// + /// True if sayve changes ensure auto record the change history. + /// The number of state entries written to the database. + int SaveChanges(bool ensureAutoHistory = false); + + /// + /// Asynchronously saves all changes made in this unit of work to the database. + /// + /// True if save changes ensure auto record the change history. + /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database. + Task SaveChangesAsync(bool ensureAutoHistory = false); + + /// + /// Executes the specified raw SQL command. + /// + /// The raw SQL. + /// The parameters. + /// The number of state entities written to database. + int ExecuteSqlCommand(string sql, params object[] parameters); + + /// + /// Uses raw SQL queries to fetch the specified data. + /// + /// The type of the entity. + /// The raw SQL. + /// The parameters. + /// An that contains elements that satisfy the condition specified by raw SQL. + IQueryable FromSql(string sql, params object[] parameters) where TEntity : class; + + /// + /// Uses TrakGrap Api to attach disconnected entities + /// + /// Root entity + /// Delegate to convert Object's State properities to Entities entry state. + void TrackGraph(object rootEntity, Action callback); + } +} diff --git a/Entities/Context/UnitOfWork/IUnitOfWorkOfT.cs b/Entities/Context/UnitOfWork/IUnitOfWorkOfT.cs new file mode 100644 index 0000000..cfe08b6 --- /dev/null +++ b/Entities/Context/UnitOfWork/IUnitOfWorkOfT.cs @@ -0,0 +1,27 @@ + + +namespace SharedDATA.Api +{ + using Microsoft.EntityFrameworkCore; + using System.Threading.Tasks; + + /// + /// Defines the interface(s) for generic unit of work. + /// + public interface IUnitOfWork : IUnitOfWork where TContext : DbContext + { + /// + /// Gets the db context. + /// + /// The instance of type . + TContext DbContext { get; } + + /// + /// Saves all changes made in this context to the database with distributed transaction. + /// + /// True if save changes ensure auto record the change history. + /// An optional array. + /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database. + Task SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks); + } +} diff --git a/Entities/Context/UnitOfWork/Repository.cs b/Entities/Context/UnitOfWork/Repository.cs new file mode 100644 index 0000000..06030e5 --- /dev/null +++ b/Entities/Context/UnitOfWork/Repository.cs @@ -0,0 +1,942 @@ + + +namespace SharedDATA.Api +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Metadata; + using Microsoft.EntityFrameworkCore.Query; + using Microsoft.EntityFrameworkCore.ChangeTracking; + using SharedDATA.Context; + + /// + /// Represents a default generic repository implements the interface. + /// + /// The type of the entity. + public class Repository : IRepository where TEntity : class + { + protected readonly DbContext _dbContext; + protected readonly DbSet _dbSet; + + /// + /// Initializes a new instance of the class. + /// + /// The database context. + public Repository(DbContext dbContext) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + _dbSet = _dbContext.Set(); + } + + /// + /// Changes the table name. This require the tables in the same database. + /// + /// + /// + /// This only been used for supporting multiple tables in the same model. This require the tables in the same database. + /// + public virtual void ChangeTable(string table) + { + if (_dbContext.Model.FindEntityType(typeof(TEntity)) is IConventionEntityType relational) + { + relational.SetTableName(table); + } + } + + /// + /// Gets all entities. This method is not recommended + /// + /// The . + public IQueryable GetAll() + { + return _dbSet; + } + + /// + /// Gets all entities. This method is not recommended + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// Ex: This method defaults to a read-only, no-tracking query. + public IQueryable GetAll( + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, bool disableTracking = true, bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query); + } + else + { + return query; + } + } + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// The index of page. + /// The size of the page. + /// True to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + public virtual IPagedList GetPagedList(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query).ToPagedList(pageIndex, pageSize); + } + else + { + return query.ToPagedList(pageIndex, pageSize); + } + } + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// The index of page. + /// The size of the page. + /// True to disable changing tracking; otherwise, false. Default to true. + /// + /// A to observe while waiting for the task to complete. + /// + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + public virtual Task> GetPagedListAsync(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + CancellationToken cancellationToken = default(CancellationToken), + bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query).ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken); + } + else + { + return query.ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken); + } + } + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// The index of page. + /// The size of the page. + /// True to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + public virtual IPagedList GetPagedList(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + bool ignoreQueryFilters = false) + where TResult : class + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query).Select(selector).ToPagedList(pageIndex, pageSize); + } + else + { + return query.Select(selector).ToPagedList(pageIndex, pageSize); + } + } + + /// + /// Gets the based on a predicate, orderby delegate and page information. This method default no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// The index of page. + /// The size of the page. + /// True to disable changing tracking; otherwise, false. Default to true. + /// + /// A to observe while waiting for the task to complete. + /// + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + public virtual Task> GetPagedListAsync(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + int pageIndex = 0, + int pageSize = 20, + bool disableTracking = true, + CancellationToken cancellationToken = default(CancellationToken), + bool ignoreQueryFilters = false) + where TResult : class + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query).Select(selector).ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken); + } + else + { + return query.Select(selector).ToPagedListAsync(pageIndex, pageSize, 0, cancellationToken); + } + } + + /// + /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query. + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// True to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + public virtual TEntity GetFirstOrDefault(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query).FirstOrDefault(); + } + else + { + return query.FirstOrDefault(); + } + } + + + /// + public virtual async Task GetFirstOrDefaultAsync(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return await orderBy(query).FirstOrDefaultAsync(); + } + else + { + return await query.FirstOrDefaultAsync(); + } + } + + /// + /// Gets the first or default entity based on a predicate, orderby delegate and include delegate. This method default no-tracking query. + /// + /// The selector for projection. + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// True to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// This method default no-tracking query. + public virtual TResult GetFirstOrDefault(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, + bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return orderBy(query).Select(selector).FirstOrDefault(); + } + else + { + return query.Select(selector).FirstOrDefault(); + } + } + + /// + public virtual async Task GetFirstOrDefaultAsync(Expression> selector, + Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return await orderBy(query).Select(selector).FirstOrDefaultAsync(); + } + else + { + return await query.Select(selector).FirstOrDefaultAsync(); + } + } + + /// + /// Uses raw SQL queries to fetch the specified data. + /// + /// The raw SQL. + /// The parameters. + /// An that contains elements that satisfy the condition specified by raw SQL. + public virtual IQueryable FromSql(string sql, params object[] parameters) => _dbSet.FromSqlRaw(sql, parameters); + + /// + /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned. + /// + /// The values of the primary key for the entity to be found. + /// The found entity or null. + public virtual TEntity Find(params object[] keyValues) => _dbSet.Find(keyValues); + + /// + /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned. + /// + /// The values of the primary key for the entity to be found. + /// A that represents the asynchronous insert operation. + public virtual ValueTask FindAsync(params object[] keyValues) => _dbSet.FindAsync(keyValues); + + /// + /// Finds an entity with the given primary key values. If found, is attached to the context and returned. If no entity is found, then null is returned. + /// + /// The values of the primary key for the entity to be found. + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous find operation. The task result contains the found entity or null. + public virtual ValueTask FindAsync(object[] keyValues, CancellationToken cancellationToken) => _dbSet.FindAsync(keyValues, cancellationToken); + + /// + /// Gets the count based on a predicate. + /// + /// + /// + public virtual int Count(Expression> predicate = null) + { + if (predicate == null) + { + return _dbSet.Count(); + } + else + { + return _dbSet.Count(predicate); + } + } + + /// + /// Gets async the count based on a predicate. + /// + /// + /// + public virtual async Task CountAsync(Expression> predicate = null) + { + if (predicate == null) + { + return await _dbSet.CountAsync(); + } + else + { + return await _dbSet.CountAsync(predicate); + } + } + + /// + /// Gets the long count based on a predicate. + /// + /// + /// + public virtual long LongCount(Expression> predicate = null) + { + if (predicate == null) + { + return _dbSet.LongCount(); + } + else + { + return _dbSet.LongCount(predicate); + } + } + + /// + /// Gets async the long count based on a predicate. + /// + /// + /// + public virtual async Task LongCountAsync(Expression> predicate = null) + { + if (predicate == null) + { + return await _dbSet.LongCountAsync(); + } + else + { + return await _dbSet.LongCountAsync(predicate); + } + } + + /// + /// Gets the max based on a predicate. + /// + /// + /// /// + /// decimal + public virtual T Max(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return _dbSet.Max(selector); + else + return _dbSet.Where(predicate).Max(selector); + } + + /// + /// Gets the async max based on a predicate. + /// + /// + /// /// + /// decimal + public virtual async Task MaxAsync(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return await _dbSet.MaxAsync(selector); + else + return await _dbSet.Where(predicate).MaxAsync(selector); + } + + /// + /// Gets the min based on a predicate. + /// + /// + /// /// + /// decimal + public virtual T Min(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return _dbSet.Min(selector); + else + return _dbSet.Where(predicate).Min(selector); + } + + /// + /// Gets the async min based on a predicate. + /// + /// + /// /// + /// decimal + public virtual async Task MinAsync(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return await _dbSet.MinAsync(selector); + else + return await _dbSet.Where(predicate).MinAsync(selector); + } + + /// + /// Gets the average based on a predicate. + /// + /// + /// /// + /// decimal + public virtual decimal Average(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return _dbSet.Average(selector); + else + return _dbSet.Where(predicate).Average(selector); + } + + /// + /// Gets the async average based on a predicate. + /// + /// + /// /// + /// decimal + public virtual async Task AverageAsync(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return await _dbSet.AverageAsync(selector); + else + return await _dbSet.Where(predicate).AverageAsync(selector); + } + + /// + /// Gets the sum based on a predicate. + /// + /// + /// /// + /// decimal + public virtual decimal Sum(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return _dbSet.Sum(selector); + else + return _dbSet.Where(predicate).Sum(selector); + } + + /// + /// Gets the async sum based on a predicate. + /// + /// + /// /// + /// decimal + public virtual async Task SumAsync(Expression> predicate = null, Expression> selector = null) + { + if (predicate == null) + return await _dbSet.SumAsync(selector); + else + return await _dbSet.Where(predicate).SumAsync(selector); + } + + /// + /// Gets the exists based on a predicate. + /// + /// + /// + public bool Exists(Expression> selector = null) + { + if (selector == null) + { + return _dbSet.Any(); + } + else + { + return _dbSet.Any(selector); + } + } + /// + /// Gets the async exists based on a predicate. + /// + /// + /// + public async Task ExistsAsync(Expression> selector = null) + { + if (selector == null) + { + return await _dbSet.AnyAsync(); + } + else + { + return await _dbSet.AnyAsync(selector); + } + } + /// + /// Inserts a new entity synchronously. + /// + /// The entity to insert. + public virtual TEntity Insert(TEntity entity) + { + return _dbSet.Add(entity).Entity; + } + + /// + /// Inserts a range of entities synchronously. + /// + /// The entities to insert. + public virtual void Insert(params TEntity[] entities) => _dbSet.AddRange(entities); + + /// + /// Inserts a range of entities synchronously. + /// + /// The entities to insert. + public virtual void Insert(IEnumerable entities) => _dbSet.AddRange(entities); + + /// + /// Inserts a new entity asynchronously. + /// + /// The entity to insert. + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous insert operation. + public virtual ValueTask> InsertAsync(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)) + { + return _dbSet.AddAsync(entity, cancellationToken); + + // Shadow properties? + //var property = _dbContext.Entry(entity).Property("Created"); + //if (property != null) { + //property.CurrentValue = DateTime.Now; + //} + } + + /// + /// Inserts a range of entities asynchronously. + /// + /// The entities to insert. + /// A that represents the asynchronous insert operation. + public virtual Task InsertAsync(params TEntity[] entities) => _dbSet.AddRangeAsync(entities); + + /// + /// Inserts a range of entities asynchronously. + /// + /// The entities to insert. + /// A to observe while waiting for the task to complete. + /// A that represents the asynchronous insert operation. + public virtual Task InsertAsync(IEnumerable entities, CancellationToken cancellationToken = default(CancellationToken)) => _dbSet.AddRangeAsync(entities, cancellationToken); + + /// + /// Updates the specified entity. + /// + /// The entity. + public virtual void Update(TEntity entity) + { + _dbSet.Update(entity); + } + + /// + /// Updates the specified entity. + /// + /// The entity. + public virtual void UpdateAsync(TEntity entity) + { + _dbSet.Update(entity); + + } + + /// + /// Updates the specified entities. + /// + /// The entities. + public virtual void Update(params TEntity[] entities) => _dbSet.UpdateRange(entities); + + /// + /// Updates the specified entities. + /// + /// The entities. + public virtual void Update(IEnumerable entities) => _dbSet.UpdateRange(entities); + + /// + /// Deletes the specified entity. + /// + /// The entity to delete. + public virtual void Delete(TEntity entity) => _dbSet.Remove(entity); + + /// + /// Deletes the entity by the specified primary key. + /// + /// The primary key value. + public virtual void Delete(object id) + { + // using a stub entity to mark for deletion + var typeInfo = typeof(TEntity).GetTypeInfo(); + var key = _dbContext.Model.FindEntityType(typeInfo).FindPrimaryKey().Properties.FirstOrDefault(); + var property = typeInfo.GetProperty(key?.Name); + if (property != null) + { + var entity = Activator.CreateInstance(); + property.SetValue(entity, id); + _dbContext.Entry(entity).State = EntityState.Deleted; + } + else + { + var entity = _dbSet.Find(id); + if (entity != null) + { + Delete(entity); + } + } + } + + /// + /// Deletes the specified entities. + /// + /// The entities. + public virtual void Delete(params TEntity[] entities) => _dbSet.RemoveRange(entities); + + /// + /// Deletes the specified entities. + /// + /// The entities. + public virtual void Delete(IEnumerable entities) => _dbSet.RemoveRange(entities); + + /// + /// Gets all entities. This method is not recommended + /// + /// The . + public async Task> GetAllAsync() + { + return await _dbSet.ToListAsync(); + } + + /// + /// Gets all entities. This method is not recommended + /// + /// A function to test each element for a condition. + /// A function to order elements. + /// A function to include navigation properties + /// true to disable changing tracking; otherwise, false. Default to true. + /// Ignore query filters + /// An that contains elements that satisfy the condition specified by . + /// Ex: This method defaults to a read-only, no-tracking query. + public async Task> GetAllAsync(Expression> predicate = null, + Func, IOrderedQueryable> orderBy = null, + Func, IIncludableQueryable> include = null, + bool disableTracking = true, bool ignoreQueryFilters = false) + { + IQueryable query = _dbSet; + + if (disableTracking) + { + query = query.AsNoTracking(); + } + + if (include != null) + { + query = include(query); + } + + if (predicate != null) + { + query = query.Where(predicate); + } + + if (ignoreQueryFilters) + { + query = query.IgnoreQueryFilters(); + } + + if (orderBy != null) + { + return await orderBy(query).ToListAsync(); + } + else + { + return await query.ToListAsync(); + } + } + + /// + /// Change entity state for patch method on web api. + /// + /// The entity. + /// /// The entity state. + public void ChangeEntityState(TEntity entity, EntityState state) + { + _dbContext.Entry(entity).State = state; + } + } +} diff --git a/Entities/Context/UnitOfWork/UnitOfWork.cs b/Entities/Context/UnitOfWork/UnitOfWork.cs new file mode 100644 index 0000000..97b079c --- /dev/null +++ b/Entities/Context/UnitOfWork/UnitOfWork.cs @@ -0,0 +1,225 @@ + +namespace SharedDATA.Api +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Linq; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using System.Transactions; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.ChangeTracking; + using Microsoft.EntityFrameworkCore.Infrastructure; + using Microsoft.EntityFrameworkCore.Metadata; + + /// + /// Represents the default implementation of the and interface. + /// + /// The type of the db context. + public class UnitOfWork : IRepositoryFactory, IUnitOfWork, IUnitOfWork where TContext : DbContext + { + private readonly TContext _context; + private bool disposed = false; + private Dictionary repositories; + + /// + /// Initializes a new instance of the class. + /// + /// The context. + public UnitOfWork(TContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + /// + /// Gets the db context. + /// + /// The instance of type . + public TContext DbContext => _context; + + /// + /// Changes the database name. This require the databases in the same machine. NOTE: This only work for MySQL right now. + /// + /// The database name. + /// + /// This only been used for supporting multiple databases in the same model. This require the databases in the same machine. + /// + public void ChangeDatabase(string database) + { + var connection = _context.Database.GetDbConnection(); + if (connection.State.HasFlag(ConnectionState.Open)) + { + connection.ChangeDatabase(database); + } + else + { + var connectionString = Regex.Replace(connection.ConnectionString.Replace(" ", ""), @"(?<=[Dd]atabase=)\w+(?=;)", database, RegexOptions.Singleline); + connection.ConnectionString = connectionString; + } + + // Following code only working for mysql. + var items = _context.Model.GetEntityTypes(); + foreach (var item in items) + { + if (item is IConventionEntityType entityType) + { + entityType.SetSchema(database); + } + } + } + + /// + /// Gets the db context. + /// + /// + public TDbContext GetDbContext() where TDbContext : DbContext + { + if (_context == null) + return null; + return _context as TDbContext; + } + + /// + /// Gets the specified repository for the . + /// + /// True if providing custom repositry + /// The type of the entity. + /// An instance of type inherited from interface. + public IRepository GetRepository(bool hasCustomRepository = false) where TEntity : class + { + if (repositories == null) + { + repositories = new Dictionary(); + } + + // what's the best way to support custom reposity? + if (hasCustomRepository) + { + var customRepo = _context.GetService>(); + if (customRepo != null) + { + return customRepo; + } + } + + var type = typeof(TEntity); + if (!repositories.ContainsKey(type)) + { + repositories[type] = new Repository(_context); + } + + return (IRepository)repositories[type]; + } + + /// + /// Executes the specified raw SQL command. + /// + /// The raw SQL. + /// The parameters. + /// The number of state entities written to database. + public int ExecuteSqlCommand(string sql, params object[] parameters) => _context.Database.ExecuteSqlRaw(sql, parameters); + + /// + /// Uses raw SQL queries to fetch the specified data. + /// + /// The type of the entity. + /// The raw SQL. + /// The parameters. + /// An that contains elements that satisfy the condition specified by raw SQL. + public IQueryable FromSql(string sql, params object[] parameters) where TEntity : class => _context.Set().FromSqlRaw(sql, parameters); + + /// + /// Saves all changes made in this context to the database. + /// + /// True if save changes ensure auto record the change history. + /// The number of state entries written to the database. + public int SaveChanges(bool ensureAutoHistory = false) + { + if (ensureAutoHistory) + { + _context.EnsureAutoHistory(); + } + + return _context.SaveChanges(); + } + + /// + /// Asynchronously saves all changes made in this unit of work to the database. + /// + /// True if save changes ensure auto record the change history. + /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database. + public async Task SaveChangesAsync(bool ensureAutoHistory = false) + { + if (ensureAutoHistory) + { + _context.EnsureAutoHistory(); + } + + return await _context.SaveChangesAsync(); + } + + /// + /// Saves all changes made in this context to the database with distributed transaction. + /// + /// True if save changes ensure auto record the change history. + /// An optional array. + /// A that represents the asynchronous save operation. The task result contains the number of state entities written to database. + public async Task SaveChangesAsync(bool ensureAutoHistory = false, params IUnitOfWork[] unitOfWorks) + { + using (var ts = new TransactionScope()) + { + var count = 0; + foreach (var unitOfWork in unitOfWorks) + { + count += await unitOfWork.SaveChangesAsync(ensureAutoHistory); + } + + count += await SaveChangesAsync(ensureAutoHistory); + + ts.Complete(); + + return count; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// The disposing. + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + // clear repositories + if (repositories != null) + { + repositories.Clear(); + } + + // dispose the db context. + _context.Dispose(); + } + } + + disposed = true; + } + + public void TrackGraph(object rootEntity, Action callback) + { + _context.ChangeTracker.TrackGraph(rootEntity, callback); + } + } +} diff --git a/Entities/Context/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs b/Entities/Context/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs new file mode 100644 index 0000000..e34e305 --- /dev/null +++ b/Entities/Context/UnitOfWork/UnitOfWorkServiceCollectionExtensions.cs @@ -0,0 +1,118 @@ + + +namespace SharedDATA.Api +{ + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.DependencyInjection; + + /// + /// Extension methods for setting up unit of work related services in an . + /// + public static class UnitOfWorkServiceCollectionExtensions + { + /// + /// Registers the unit of work given context as a service in the . + /// + /// The type of the db context. + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + /// + /// This method only support one db context, if been called more than once, will throw exception. + /// + public static IServiceCollection AddUnitOfWork(this IServiceCollection services) where TContext : DbContext + { + services.AddScoped>(); + // Following has a issue: IUnitOfWork cannot support multiple dbcontext/database, + // that means cannot call AddUnitOfWork multiple times. + // Solution: check IUnitOfWork whether or null + services.AddScoped>(); + services.AddScoped, UnitOfWork>(); + + return services; + } + + /// + /// Registers the unit of work given context as a service in the . + /// + /// The type of the db context. + /// The type of the db context. + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + /// + /// This method only support one db context, if been called more than once, will throw exception. + /// + public static IServiceCollection AddUnitOfWork(this IServiceCollection services) + where TContext1 : DbContext + where TContext2 : DbContext + { + services.AddScoped, UnitOfWork>(); + services.AddScoped, UnitOfWork>(); + + return services; + } + + /// + /// Registers the unit of work given context as a service in the . + /// + /// The type of the db context. + /// The type of the db context. + /// The type of the db context. + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + /// + /// This method only support one db context, if been called more than once, will throw exception. + /// + public static IServiceCollection AddUnitOfWork(this IServiceCollection services) + where TContext1 : DbContext + where TContext2 : DbContext + where TContext3 : DbContext + { + services.AddScoped, UnitOfWork>(); + services.AddScoped, UnitOfWork>(); + services.AddScoped, UnitOfWork>(); + + return services; + } + + /// + /// Registers the unit of work given context as a service in the . + /// + /// The type of the db context. + /// The type of the db context. + /// The type of the db context. + /// The type of the db context. + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + /// + /// This method only support one db context, if been called more than once, will throw exception. + /// + public static IServiceCollection AddUnitOfWork(this IServiceCollection services) + where TContext1 : DbContext + where TContext2 : DbContext + where TContext3 : DbContext + where TContext4 : DbContext + { + services.AddScoped, UnitOfWork>(); + services.AddScoped, UnitOfWork>(); + services.AddScoped, UnitOfWork>(); + services.AddScoped, UnitOfWork>(); + + return services; + } + + /// + /// Registers the custom repository as a service in the . + /// + /// The type of the entity. + /// The type of the custom repositry. + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddCustomRepository(this IServiceCollection services) + where TEntity : class + where TRepository : class, IRepository + { + services.AddScoped, TRepository>(); + return services; + } + } +} diff --git a/Entities/Contracts/Assignment.cs b/Entities/Contracts/Assignment.cs new file mode 100644 index 0000000..e4b4611 --- /dev/null +++ b/Entities/Contracts/Assignment.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("assignments")] + public class Assignment + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("title")] + [StringLength(255)] + public string Title { get; set; } + + [Column("description")] + public string Description { get; set; } + + [Required] + [Column("due_date")] + public DateTime DueDate { get; set; } + + [Column("total_points")] + public decimal? TotalPoints { get; set; } + + [Column("created_by")] + [ForeignKey("Creator")] + public Guid CreatedBy { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public User Creator { get; set; } + public ICollection AssignmentClasses { get; set; } + public ICollection AssignmentGroups { get; set; } + public ICollection AssignmentAttachments { get; set; } + public ICollection Submissions { get; set; } + + public Assignment() + { + Id = Guid.NewGuid(); + + Submissions = new HashSet(); + AssignmentGroups = new HashSet(); + AssignmentClasses = new HashSet(); + AssignmentAttachments = new HashSet(); + } + } +} diff --git a/Entities/Contracts/AssignmentAttachment.cs b/Entities/Contracts/AssignmentAttachment.cs new file mode 100644 index 0000000..a745c01 --- /dev/null +++ b/Entities/Contracts/AssignmentAttachment.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +namespace Entities.Contracts +{ + [Table("assignment_attachments")] + public class AssignmentAttachment + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("assignment_id")] + [ForeignKey("Assignment")] + public Guid AssignmentId { get; set; } + + [Required] + [Column("file_path")] + [StringLength(255)] + public string FilePath { get; set; } + + [Required] + [Column("file_name")] + [StringLength(255)] + public string FileName { get; set; } + + [Column("uploaded_at")] + public DateTime UploadedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public Assignment Assignment { get; set; } + + public AssignmentAttachment() + { + Id = Guid.NewGuid(); + } + + } +} diff --git a/Entities/Contracts/AssignmentClass.cs b/Entities/Contracts/AssignmentClass.cs new file mode 100644 index 0000000..1fc69df --- /dev/null +++ b/Entities/Contracts/AssignmentClass.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("assignment_class")] + public class AssignmentClass + { + [Key] + [Column("assignment_id", Order = 0)] + [ForeignKey("Assignment")] + public Guid AssignmentId { get; set; } + + [Key] + [Column("class_id", Order = 1)] + [ForeignKey("Class")] + public Guid ClassId { get; set; } + + [Column("assigned_at")] + public DateTime AssignedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public Assignment Assignment { get; set; } + public Class Class { get; set; } + + + } +} diff --git a/Entities/Contracts/AssignmentGroup.cs b/Entities/Contracts/AssignmentGroup.cs new file mode 100644 index 0000000..f909797 --- /dev/null +++ b/Entities/Contracts/AssignmentGroup.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("assignment_group")] + public class AssignmentGroup + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("assignment")] + [ForeignKey("Assignment")] + public Guid AssignmentId { get; set; } + + [Required] + [Column("title")] + [MaxLength(65535)] + public string Title { get; set; } + + [Column("descript")] + [MaxLength(65535)] + public string Descript { get; set; } + + + [Column("total_points")] + public decimal? TotalPoints { get; set; } + + [Column("number")] + public byte Number { get; set; } + + [Column("parent_group")] + public Guid? ParentGroup { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public Assignment Assignment { get; set; } + public AssignmentGroup ParentAssignmentGroup { get; set;} + public ICollection ChildAssignmentGroups { get; set; } + public ICollection AssignmentQuestions { get; set; } + + + public AssignmentGroup() + { + Id = Guid.NewGuid(); + ChildAssignmentGroups = new HashSet(); + AssignmentQuestions = new HashSet(); + } + } +} diff --git a/Entities/Contracts/AssignmentQuestion.cs b/Entities/Contracts/AssignmentQuestion.cs new file mode 100644 index 0000000..adb19a5 --- /dev/null +++ b/Entities/Contracts/AssignmentQuestion.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("assignment_questions")] + public class AssignmentQuestion + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("question_id")] + [ForeignKey("Question")] + public Guid QuestionId { get; set; } + + [Required] + [Column("question_number")] + public uint QuestionNumber { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Required] + [Column("detail_id")] + [ForeignKey("AssignmentGroup")] + public Guid AssignmentGroupId { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + + public Question Question { get; set; } + public ICollection SubmissionDetails { get; set; } + public AssignmentGroup AssignmentGroup { get; set; } + + public AssignmentQuestion() + { + Id = Guid.NewGuid(); + SubmissionDetails = new HashSet(); + } + } +} diff --git a/Entities/Contracts/Class.cs b/Entities/Contracts/Class.cs new file mode 100644 index 0000000..45edfee --- /dev/null +++ b/Entities/Contracts/Class.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("classes")] + public class Class + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Column("grade")] + public byte Grade { get; set; } + + [Column("class")] + public byte Number { get; set; } + + [Column("description")] + public string Description { get; set; } + + [Column("head_teacher_id")] + public Guid? HeadTeacherId { get; set; } + public User HeadTeacher { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public ICollection ClassTeachers { get; set; } + public ICollection ClassStudents { get; set; } + public ICollection AssignmentClasses { get; set; } + + public Class() + { + Id = Guid.NewGuid(); + Grade = 0; + Number = 0; + ClassStudents = new HashSet(); + ClassTeachers = new HashSet(); + AssignmentClasses = new HashSet(); + } + } +} diff --git a/Entities/Contracts/ClassStudent.cs b/Entities/Contracts/ClassStudent.cs new file mode 100644 index 0000000..6351794 --- /dev/null +++ b/Entities/Contracts/ClassStudent.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("class_student")] + public class ClassStudent + { + [Key] + [Column("class_id", Order = 0)] + [ForeignKey("Class")] + public Guid ClassId { get; set; } + + [Key] + [Column("student_id", Order = 1)] + [ForeignKey("Student")] + public Guid StudentId { get; set; } + + [Column("enrollment_date")] + public DateTime EnrollmentDate { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public Class Class { get; set; } + public User Student { get; set; } + } +} diff --git a/Entities/Contracts/ClassTeacher.cs b/Entities/Contracts/ClassTeacher.cs new file mode 100644 index 0000000..7974b37 --- /dev/null +++ b/Entities/Contracts/ClassTeacher.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("class_teachers")] + public class ClassTeacher + { + [Key] + [Column("class_id")] + public Guid ClassId { get; set; } + public Class Class { get; set; } + + [Key] + [Column("teacher_id")] + public Guid TeacherId { get; set; } + public User Teacher { get; set; } + + [Column("subject_taught")] + public string SubjectTaught { get; set; } + } +} diff --git a/Entities/Contracts/Question.cs b/Entities/Contracts/Question.cs new file mode 100644 index 0000000..e09c247 --- /dev/null +++ b/Entities/Contracts/Question.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("questions")] + public class Question + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("question_text")] + [MaxLength(65535)] + public string QuestionText { get; set; } + + [Required] + [Column("question_type")] + [MaxLength(20)] + public QuestionType QuestionType { get; set; } + + [Column("correct_answer")] + [MaxLength(65535)] + public string CorrectAnswer { get; set; } + + [Column("difficulty_level")] + [MaxLength(10)] + public DifficultyLevel DifficultyLevel { get; set; } + + [Column("subject_area")] + public SubjectAreaEnum SubjectArea { get; set; } + + [Required] + [Column("created_by")] + [ForeignKey("Creator")] + public Guid CreatedBy { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public User Creator { get; set; } + public ICollection AssignmentQuestions { get; set; } + + public Question() + { + Id = Guid.NewGuid(); + AssignmentQuestions = new HashSet(); + } + } + + public enum DifficultyLevel + { + easy, + medium, + hard + } + + public enum QuestionType + { + Unknown, // 可以有一个未知类型或作为默认 + Spelling, // 拼写 + Pronunciation, // 给带点字选择正确读音 + WordFormation, // 组词 + FillInTheBlanks, // 选词填空 / 补充词语 + SentenceDictation, // 默写句子 + SentenceRewriting, // 仿句 / 改写句子 + ReadingComprehension, // 阅读理解 + Composition // 作文 + // ... 添加您其他题目类型 + } + + public enum SubjectAreaEnum // 建议命名为 SubjectAreaEnum 以避免与属性名冲突 + { + Unknown, // 未知或默认 + Mathematics, // 数学 + Physics, // 物理 + Chemistry, // 化学 + Biology, // 生物 + History, // 历史 + Geography, // 地理 + Literature, // 语文/文学 + English, // 英语 + ComputerScience, // 计算机科学 + // ... 你可以根据需要添加更多科目 + } +} diff --git a/Entities/Contracts/Submission.cs b/Entities/Contracts/Submission.cs new file mode 100644 index 0000000..2009f91 --- /dev/null +++ b/Entities/Contracts/Submission.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace Entities.Contracts +{ + [Table("submissions")] + public class Submission + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("assignment_id")] + [ForeignKey("Assignment")] + public Guid AssignmentId { get; set; } + + [Required] + [Column("student_id")] + [ForeignKey("Student")] + public Guid StudentId { get; set; } + + [Required] + [Column("attempt_number")] + public Guid AttemptNumber { get; set; } + + [Column("submission_time")] + public DateTime SubmissionTime { get; set; } + + [Column("overall_grade")] + [Precision(5, 2)] + public decimal? OverallGrade { get; set; } + + [Column("overall_feedback")] + public string OverallFeedback { get; set; } + + [Column("graded_by")] + [ForeignKey("Grader")] + public Guid? GradedBy { get; set; } + + [Column("graded_at")] + public DateTime? GradedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + [Required] + [Column("status")] + public SubmissionStatus Status { get; set; } + + // Navigation Properties + public Assignment Assignment { get; set; } + public User Student { get; set; } + public User Grader { get; set; } + public ICollection SubmissionDetails { get; set; } + + public Submission() + { + Id = Guid.NewGuid(); + SubmissionDetails = new HashSet(); + } + } + + public enum SubmissionStatus + { + Pending, // 待提交/未开始 + Submitted, // 已提交 + Graded, // 已批改 + Resubmission, // 待重新提交 (如果允许) + Late, // 迟交 + Draft, // 草稿 + // ... 添加你需要的其他状态 + } +} diff --git a/Entities/Contracts/SubmissionDetail.cs b/Entities/Contracts/SubmissionDetail.cs new file mode 100644 index 0000000..45670b7 --- /dev/null +++ b/Entities/Contracts/SubmissionDetail.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.Contracts +{ + [Table("submission_details")] + public class SubmissionDetail + { + [Key] + [Column("id")] + public Guid Id { get; set; } + + [Required] + [Column("submission_id")] + [ForeignKey("Submission")] + public Guid SubmissionId { get; set; } + + [Required] + [Column("student_id")] + [ForeignKey("User")] + public Guid StudentId { get; set; } + + [Required] + [Column("assignment_question_id")] + [ForeignKey("AssignmentQuestion")] + public Guid AssignmentQuestionId { get; set; } + + [Column("student_answer")] + public string StudentAnswer { get; set; } + + [Column("is_correct")] + public bool? IsCorrect { get; set; } + + [Column("points_awarded")] + [Precision(5, 2)] + public decimal? PointsAwarded { get; set; } + + [Column("teacher_feedback")] + public string TeacherFeedback { get; set; } + + [Column("created_at")] + public DateTime CreatedAt { get; set; } + + [Column("updated_at")] + public DateTime UpdatedAt { get; set; } + + [Column("deleted")] + public bool IsDeleted { get; set; } + + // Navigation Properties + public Submission Submission { get; set; } + public User User { get; set; } + public AssignmentQuestion AssignmentQuestion { get; set; } + + public SubmissionDetail() + { + Id = Guid.NewGuid(); + } + } +} diff --git a/Entities/Contracts/User.cs b/Entities/Contracts/User.cs new file mode 100644 index 0000000..52352db --- /dev/null +++ b/Entities/Contracts/User.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Entities.Contracts +{ + public enum UserRoles + { + Student, + Teacher, + Administrator + } + + public class User : IdentityUser + { + public string? RefreshToken { get; set; } + public DateTime? RefreshTokenExpiryTime { get; set; } + public string? Address { get; set; } + public string? DisplayName { get; set; } + + + [Column("deleted")] + public bool IsDeleted { get; set; } + + public ICollection TaughtClassesLink { get; set; } + public ICollection EnrolledClassesLink { get; set; } + + public ICollection CreatedQuestions { get; set; } + public ICollection CreatedAssignments { get; set; } + + public ICollection SubmissionDetails { get; set; } + public ICollection SubmissionsAsStudent { get; set; } + public ICollection GradedSubmissions { get; set; } + + public User() + { + Id = Guid.NewGuid(); + SecurityStamp = Guid.NewGuid().ToString(); + + CreatedQuestions = new HashSet(); + TaughtClassesLink = new HashSet(); + EnrolledClassesLink = new HashSet(); + CreatedAssignments = new HashSet(); + GradedSubmissions = new HashSet(); + SubmissionsAsStudent = new HashSet(); + } + } +} diff --git a/Entities/DTO/ApiResponse.cs b/Entities/DTO/ApiResponse.cs new file mode 100644 index 0000000..f8d9748 --- /dev/null +++ b/Entities/DTO/ApiResponse.cs @@ -0,0 +1,23 @@ +namespace TechHelper.Services +{ + public class ApiResponse + { + public ApiResponse(string message, bool status = false) + { + this.Message = message; + this.Status = status; + } + + public ApiResponse(bool status, object result) + { + this.Status = status; + this.Result = result; + } + + public string Message { get; set; } + + public bool Status { get; set; } + + public object Result { get; set; } + } +} \ No newline at end of file diff --git a/Entities/DTO/AuthResponseDto.cs b/Entities/DTO/AuthResponseDto.cs new file mode 100644 index 0000000..5ec8ae6 --- /dev/null +++ b/Entities/DTO/AuthResponseDto.cs @@ -0,0 +1,12 @@ +namespace Entities.DTO +{ + public class AuthResponseDto + { + public bool IsAuthSuccessful { get; set; } + public string? ErrorMessage { get; set; } + public string? Token { get; set; } + public string? RefreshToken { get; set; } + public bool Is2StepVerificationRequired { get; set; } + public string Provider { get; set; } + } +} diff --git a/Entities/DTO/ClassDto.cs b/Entities/DTO/ClassDto.cs new file mode 100644 index 0000000..40f5d18 --- /dev/null +++ b/Entities/DTO/ClassDto.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace Entities.DTO +{ + public class ClassDto + { + public byte Class { get; set; } + + [StringLength(50, ErrorMessage = "班级名称不能超过 50 个字符。")] + public string Name { get; set; } + + [Required(ErrorMessage = "年级是必填项。")] + [Range(1, 12, ErrorMessage = "年级编号必须在 1 到 12 之间。")] + public byte Grade { get; set; } + + public string Description { get; set; } = "HELLO WORLD"; + + + public int? HeadTeacherId { get; set; } + } +} diff --git a/Entities/DTO/ForgotPasswordDto.cs b/Entities/DTO/ForgotPasswordDto.cs new file mode 100644 index 0000000..d42cd6d --- /dev/null +++ b/Entities/DTO/ForgotPasswordDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class ForgotPasswordDto + { + [Required] + [EmailAddress] + public string Email { get; set; } + public string ClientURI { get; set; } + } +} diff --git a/Entities/DTO/RefreshTokenDto.cs b/Entities/DTO/RefreshTokenDto.cs new file mode 100644 index 0000000..0691366 --- /dev/null +++ b/Entities/DTO/RefreshTokenDto.cs @@ -0,0 +1,8 @@ +namespace Entities.DTO +{ + public class RefreshTokenDto + { + public string? Token { get; set; } + public string? RefreshToken { get; set; } + } +} diff --git a/Entities/DTO/ResetPasswordDto.cs b/Entities/DTO/ResetPasswordDto.cs new file mode 100644 index 0000000..85bf54a --- /dev/null +++ b/Entities/DTO/ResetPasswordDto.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class ResetPasswordDto + { + [Required(ErrorMessage = "Password is required")] + public string Password { get; set; } + + [Compare(nameof(Password), ErrorMessage = "The password and confirmation password do not match")] + public string ConfirmPassword { get; set; } + + public string Email { get; set; } + + public string Token { get; set; } + } +} diff --git a/Entities/DTO/ResetPasswordResponseDto.cs b/Entities/DTO/ResetPasswordResponseDto.cs new file mode 100644 index 0000000..e1eb693 --- /dev/null +++ b/Entities/DTO/ResetPasswordResponseDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class ResetPasswordResponseDto + { + public bool IsResetPasswordSuccessful { get; set; } + public IEnumerable Errors { get; set; } = Enumerable.Empty(); + } +} diff --git a/Entities/DTO/ResponseDto.cs b/Entities/DTO/ResponseDto.cs new file mode 100644 index 0000000..237af60 --- /dev/null +++ b/Entities/DTO/ResponseDto.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class ResponseDto + { + public bool IsSuccessfulRegistration { get; set; } + public IEnumerable Errors { get; set; } = Enumerable.Empty(); + } +} diff --git a/Entities/DTO/TwoFactorVerificationDto.cs b/Entities/DTO/TwoFactorVerificationDto.cs new file mode 100644 index 0000000..6cbb107 --- /dev/null +++ b/Entities/DTO/TwoFactorVerificationDto.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class TwoFactorVerificationDto + { + public string Email { get; set; } + public string Provider { get; set; } + [Required(ErrorMessage = "Token is required")] + public string TwoFactorToken { get; set; } + } +} diff --git a/Entities/DTO/UserForAuthenticationDto.cs b/Entities/DTO/UserForAuthenticationDto.cs new file mode 100644 index 0000000..fce48c4 --- /dev/null +++ b/Entities/DTO/UserForAuthenticationDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Entities.DTO +{ + public class UserForAuthenticationDto + { + [Required(ErrorMessage = "Email is required")] + public string? Email { get; set; } + [Required(ErrorMessage = "Password is required")] + public string? Password { get; set; } + } +} diff --git a/Entities/DTO/UserForRegistrationDto.cs b/Entities/DTO/UserForRegistrationDto.cs new file mode 100644 index 0000000..ede7e5b --- /dev/null +++ b/Entities/DTO/UserForRegistrationDto.cs @@ -0,0 +1,51 @@ +using Entities.Contracts; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class UserForRegistrationDto + { + [Required(ErrorMessage = "姓名是必填项。")] + [StringLength(50, MinimumLength = 2, ErrorMessage = "姓名长度必须在 2 到 50 个字符之间。")] + public string Name { get; set; } + + [Required(ErrorMessage = "电子邮件是必填项。")] + [EmailAddress(ErrorMessage = "电子邮件格式不正确。")] // 添加 Email 格式验证 + public string Email { get; set; } + + [Required(ErrorMessage = "密码是必填项。")] + [StringLength(100, MinimumLength = 6, ErrorMessage = "密码长度至少为 6 个字符。")] // 确保长度验证 + [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&.])[A-Za-z\d@$!%*?&.]{6,}$", + ErrorMessage = "密码必须包含至少一个大写字母、一个小写字母、一个数字和一个特殊字符。特殊字符包含 @$!%*?&. ")] + public string Password { get; set; } + + [Compare(nameof(Password), ErrorMessage = "密码和确认密码不匹配。")] + [Required(ErrorMessage = "确认密码是必填项。")] // 确保确认密码也必须填写 + public string ConfirmPassword { get; set; } + + [Required(ErrorMessage = "至少选择一个角色。")] + public UserRoles Roles { get; set; } = UserRoles.Student; // 根据你当前的 MudRadioGroup 设定,这里是单选 + + [Required(ErrorMessage = "班级是必填项。")] + [Range(1, 14, ErrorMessage = "班级编号必须在 1 到 14 之间。")] // 班级范围已调整为 1-14 + public int Class { get; set; } = 1; + + [Required(ErrorMessage = "年级是必填项。")] + [Range(1, 6, ErrorMessage = "年级编号必须在 1 到 6 之间。")] // 年级范围已调整为 1-6 + public int Grade { get; set; } = 1; + + [Phone(ErrorMessage = "电话号码格式不正确。")] + [StringLength(11, MinimumLength = 11, ErrorMessage = "电话号码必须是 11 位数字。")] // 电话号码长度已调整为固定 11 位 + public string PhoneNumber { get; set; } + + [StringLength(200, ErrorMessage = "家庭住址不能超过 200 个字符。")] + public string HomeAddress { get; set; } + + public string ClientURI { get; set; } + } +} diff --git a/Entities/DTO/UserRegistrationToClassDto.cs b/Entities/DTO/UserRegistrationToClassDto.cs new file mode 100644 index 0000000..84ba109 --- /dev/null +++ b/Entities/DTO/UserRegistrationToClassDto.cs @@ -0,0 +1,18 @@ +using Entities.Contracts; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Entities.DTO +{ + public class UserRegistrationToClassDto + { + public string User { get; set; } + public byte ClassId { get; set; } + public byte GradeId { get; set; } + public UserRoles Roles { get; set; } = UserRoles.Student; + public SubjectAreaEnum SubjectArea { get; set; } = SubjectAreaEnum.Unknown; + } +} diff --git a/Entities/Entities.csproj b/Entities/Entities.csproj new file mode 100644 index 0000000..35740ab --- /dev/null +++ b/Entities/Entities.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + diff --git a/Entities/RequestFeatures/MetaData.cs b/Entities/RequestFeatures/MetaData.cs new file mode 100644 index 0000000..5086eec --- /dev/null +++ b/Entities/RequestFeatures/MetaData.cs @@ -0,0 +1,12 @@ +namespace Entities.RequestFeatures +{ + public class MetaData + { + public int CurrentPage { get; set; } + public int TotalPages { get; set; } + public int PageSize { get; set; } + public int TotalCount { get; set; } + public bool HasPrevious => CurrentPage > 1; + public bool HasNext => CurrentPage < TotalPages; + } +} diff --git a/Entities/RequestFeatures/ProductParameters.cs b/Entities/RequestFeatures/ProductParameters.cs new file mode 100644 index 0000000..3d55684 --- /dev/null +++ b/Entities/RequestFeatures/ProductParameters.cs @@ -0,0 +1,22 @@ +namespace Entities.RequestFeatures +{ + public class ProductParameters + { + const int maxPageSize = 50; + public int PageNumber { get; set; } = 1; + private int _pageSize = 4; + public int PageSize + { + get + { + return _pageSize; + } + set + { + _pageSize = (value > maxPageSize) ? maxPageSize : value; + } + } + public string? SearchTerm { get; set; } + public string OrderBy { get; set; } = "name"; + } +} diff --git a/TechHelper.Client/AI/AIConfiguration.cs b/TechHelper.Client/AI/AIConfiguration.cs new file mode 100644 index 0000000..760f312 --- /dev/null +++ b/TechHelper.Client/AI/AIConfiguration.cs @@ -0,0 +1,108 @@ +namespace TechHelper.Client.AI +{ + public static class AIConfiguration + { + public static string APIkey = "c62e38f2b74e47a080487a7a2fef014a.Q6Gx5HBy6Pj0OhkI"; + + public static string ExamAnsConfig = "你是一个专业的试题生成助手,请根据提供的科目、年级和主题," + + "严格按照下方指定的 JSON 格式和内容生成规则,输出高质量的试题数据。\r\n\r\n---\r\n**输出格式要求:" + + "**\r\n\r\n请严格输出一个 **JSON 数组**。数组的每个元素都是一个**试题对象**。\r\n\r\n```json\r\n[\r\n " + + "{\r\n \"题号\": \"X\",\r\n \"标题\": \"XXXXXX\",\r\n \"分值\": N,\r\n \"分值问题标记\": \"(若" + + "分值存在问题,请在此处简要说明,例如:'该题分值分配需复核';否则,此字段留空)\",\r\n \"题目引用\": \"(若" + + "为阅读理解等引用原文的题型,请在此处补充完整原文内容,并支持换行,例如:'(一)葡萄沟(节选)\\\\n葡萄种在山坡上" + + "的梯田里...';否则,此字段留空)\",\r\n \"子题目\": [\r\n {\r\n \"子题号\": \"X\",\r\n " + + " \"题干\": \"XXXXXXX(独立成题,例如:'一( )大象';或拼音题:'pú táo( )';或成语填空题的单个成语:'( )心( )意')\",\r\n " + + " \"分值\": N,\r\n \"分值问题标记\": \"(若子题分值存在问题,请在此处简要说明,例如:'该子题分值偏高';否则,此字段留空)\",\r\n " + + " \"选项\": [\"A\", \"B\", \"C\"],\r\n \"示例答案\": \"XXXXXXX\"\r\n }\r\n ],\r\n \"子" + + "题组\": []\r\n }\r\n]\r\n内容生成规则:\r\n\r\n强制拆分独立考察点:\r\n核心规则: 如果一个概念题干中包含多个独立的、" + + "可分别作答或评分的部分(例如:多个量词填空、每个成语填空、每个拼音写词语),必须将其拆分为各自独立的 子题目 对象。\r\n示" + + "例:\r\n原题干:“一( )大象 一( )大船 一( )菜叶” → 拆分为 3 个 子题目。\r\n原题干:“看拼音,写词语:pú táo( )、xiāng " + + "jiāo( )” → 拆分为 2 个 子题目。\r\n每个拆分后的 子题目 必须有独立的 子题号 和 分值。\r\n合并重复题号:\r\n在 JSON 数组中,如果多" + + "个题组(如阅读理解下的不同文段 (一)、(二))属于同一大题,它们可以有相同的 题号 字段值。但每个题组必须是 JSON 数组中的一个独" + + "立对象。\r\n题目引用完整性:\r\n若有引用文本(如阅读理解的原文),必须在 题目引用 字段中补充完整原文内容。支持 \\n 进行换行。如果" + + "无引用文本,此字段留空 \"\"。\r\n分值分配与转换:\r\n所有 子题目 的 分值 总和必须等于对应大题(QuestionGroup)的 分值。\r\n如果原始" + + "分值以百分比表示,必须将其转换为具体的数值。\r\n选择题的每个选项的分值必须相同。\r\n标点规范:\r\n所有中文标点统一使用全角符号。\r\n长文" + + "本换行:\r\n题目引用、题干 等长文本内容应自动换行,每行不超过 80 个字符(使用 \\n)。\r\n特殊处理要求:\r\n\r\n选择题 (选项 字段" + + "不为空):\r\n选项 字段必须包含一个字符串数组,格式为 [\"选项A内容\", \"选项B内容\", \"选项C内容\"]。\r\n示例答案 字段必须提供一个具体" + + "的示例答案。\r\n填空题 (选项 字段为空):\r\n示例答案 字段必须提供一个具体的示例答案(例如:“纷纷扬扬”)。\r\n阅读理解统计题(例如:句" + + "数/小节数): 需在 题干 中明确标注单位。\r\n主旨题(若需多选): 请在 题干 描述中清晰体现其多选特性。\r\n子题组 字段: 在当前 JSON 结构中,子" + + "题组 字段应始终保持为空数组 [],除非我另行通知需要更深层次的题组嵌套。\r\n格式验证要求(模型内部验证,确保合规性):\r\n\r\n题目引用 字" + + "段若为空字符串 \"\",表示该题型无原文引用。\r\n分值问题标记 字段若不为空字符串 \"\",表示该分值需人工复核,模型无需自动修改分值。\r\n禁止 " + + "子题目 与 题目引用 以外的内容混杂在 题干 或 标题 中。\r\n每个 子题目 必须独立成题,具备独立的 分值 和 题干。\r\nJSON 语法必须正确," + + "能够通过任何在线 JSON 校验工具的验证。\r\n所有分值分配合理(子题目 总分等于大题 分值),且百分比分值已转换为具体数值。\r\n所有标" + + "点符号 100% 使用中文全角。\r\n长文本自动换行(每行不超过 80 字符,使用 \\n)。\r\n题号 合并与 子题目 区分应通过 JSON 数组中的独立对象体现。"; + + public static string ExamToXML = "文本试卷解析请求模板 你只需要给出转换后的结果,不需要其他任何无关的输出\r\n请将我提供的" + + "文本试卷内容,按照以下精简版 XAML 风格的标记规则进行解析和结构化输出。\r\n\r\n输出" + + "格式要求:\r\n请以精简版 XAML 风格的文本标记形式输出,并确保以下标签和属性的正确" + + "使用:\r\n\r\n根元素: \r\n大题: \r\nId: 大题题号(应从文本中提取)。\r\nT: 大题标题(应从文本中提取)。\r\nS: 大题总分" + + "(应从文本中提取,百分比请转换为具体数值)。\r\nSPM: 分值问题标记(如果文本中无则为空字符串 \"\";如果有任何" + + "分值分配上的疑问或需要复核,请在此标记)。\r\n题目引用: 引用文本 (如果原始文本中无引用部分,则省" + + "略此标签)\r\n子题目列表容器: \r\n子题目: \r\nId: 子题号(应从文本中提取,例如 \"1.1\", \"2.3a\")。\r\nT: 子题目题干。\r\n【核心解析规则】 除了明显的拼" + + "写和成语填空类题目外,所有子题的 T 属性都必须包含完整的原始题目表述,包括所有选项、括号等,以最大程度地保留原始文本结构。\r\nS: 子题" + + "分值(应从文本中提取)。\r\nSPM: 分值问题标记(如果文本中无则为空字符串 \"\";如果有任何分值分配上的疑问或需要复核,请在此标记)。\r\nSA: 示例" + + "答案。\r\n对于有多个填空或判断的题目(其 T 属性包含多个空位),答案请用空格分隔,并按题干中出现的顺序排列。\r\n示例: SA=\"× √ √\" (对应多" + + "个判断)\r\n示例: SA=\"“ 哪 ? ” 。\" (对应多个标点填空)\r\n选项列表容器: (仅用于原始文本中明确给出选项的选择题,如果无选项则省" + + "略此标签)\r\n选项: (选项值应从原始文本中提取)\r\n子题组列表容器: (如果原始文本中无嵌套题组,则为空标签)\r\n内容解" + + "析规则:\r\n准确识别大题与子题: 根据题号、标题和分值模式,准确识别并划分大题 () 和其下的子题目 ()。\r\n细致拆分独立" + + "考察点: 将文本中所有可独立评分或作答的部分,尽可能地拆分为独立的 标记块。但请注意,对于单个逻辑题但包含多个填空/选项/判断点(如填空" + + "题和判断题),请将所有相关内容合并到一个 的 T 属性中,并提供序列化的 SA。\r\n提取题目引用: 识别阅读理解等题型中的引用段落,并" + + "将其完整放入 标签中。\r\n精确提取分值: 从原文中提取大题和子题的分值,并将其转换为阿拉伯数字。如果原文是百分比,请转换为具体分数。\r\n规范标" + + "点: 确保所有中文标点在输出中统一使用全角符号。"; + + public static string ExamToXML2 = "文本试卷解析请求模板 你只需要给出转换后的结果,不需要其他任何无关的输出 " + + " 1. 整体结构与根元素\r\n (试卷):\r\n\r\n根元素,表示一份完整的试卷。\r\n作为最外层的容器,所有试卷内容都" + + "将嵌套在其内部。\r\n没有属性,其直接子元素必须是 。\r\n (题组集合):\r\n\r\n作为 的直接子" + + "元素。\r\n没有属性,其内容将是一个或多个 标签。\r\n2. 大题/题组的标记 ()\r\n\r\n目的:用于标记试卷中的每一个“大题”或“题组”。\r\n属性要求:\r\nId:必填。从文本" + + "中提取的大题题号(例如:“一”、“I”、“1.”)。\r\nT:必填。从文本中提取的大题标题(例如:“选择题”、“填空题”)。\r\nS:必" + + "填。从文本中提取的大题总分。\r\n转换规则:如果原始文本是百分比(例如:“20%”),请转换为具体数值(例如:“20”)。\r\nSPM:可" + + "选。\r\n如果文本中没有特殊标记,则为空字符串 \"\"。\r\n用途:用于标记在分值分配上存在疑问或需要复核的大题。\r\nQR:可选。\r\n用" + + "途:如果原始文本中包含阅读理解、材料分析等需要引用的段落,请将完整的引用文本内容放入此属性。\r\n省略规则:如果原始文本中没有引用" + + "部分,则整个 QR 属性应被省略(不要保留空属性或空标签)。\r\n子元素:\r\n一个 标签内部可以包含 (用于普通子题)或 (用" + + "于嵌套题组)。两者不能同时存在。\r\n3. 子题目与选项的标记 ( & )\r\n (子题目列表容器):\r\n\r\n目的:作为 的子元素,用" + + "于封装一个大题下的所有独立小题。\r\n没有属性,其内容将是一个或多个 标签。\r\n (子题目):\r\n\r\n目的:用于标记试卷中的每一个“小题”。\r\n属性要求:\r\nId:必填。从文本中提取的子题号(例" + + "如:“1.1”、“2.3a”、“(1)”)。\r\nT:必填。子题目的完整题干内容。\r\n核心解析规则:除了明显的单个填空(例如:“C#是一个____语言。”)和" + + "单个判断类题目(例如:“是/否判断题。”)外,所有子题的 T 属性必须包含完整的原始题目表述,包括所有选项文字、括号、多个空位等,以最大程度" + + "地保留原始文本结构。\r\nS:必填。从文本中提取的子题分值。\r\nSPM:可选。规则同 中的 SPM 属性。\r\nSA:必填。小题的示例答案。\r\n多" + + "空/多判断:对于包含多个空位或判断点的题目,答案请用空格分隔,并按照题干中出现的顺序排列。\r\n示例:SA=\"× √ √\" (对应多个判断)\r\n示" + + "例:SA=\"“ 哪 ? ” 。\" (对应多个标点填空)\r\n示例:SA=\"北京 长城\" (对应两个填空)\r\n选择题:填写正确选项的字母或编号,例如 SA=\"A\" 或" + + " SA=\"A,C\"。\r\n单个填空/判断题:直接填写答案,例如 SA=\"C#\" 或 SA=\"正确\"。\r\n (选项列表容器):\r\n\r\n目的:作为 的子" + + "元素,用于封装选择题的选项。\r\n限制:仅用于原始文本中明确给出选项的选择题。\r\n省略规则:如果原始文本中无选项(例如:填空题、简答题)," + + "则整个 标签应被省略。\r\n没有属性,其内容将是一个或多个 标签。\r\n (选项):\r\n\r\n目的:标记单个选项。\r\n属" + + "性要求:\r\nV:必填。选项的完整内容,应从原始文本中提取,包括选项的字母/编号(例如:“A. 选项A”、“B) 选项B”)。\r\n4. 子题组的" + + "标记 ()\r\n (子题组列表容器):\r\n目的:作为 的子元素,用于表示嵌套的题组结构(例如:一个大题下面又包含几个小的大" + + "题组)。\r\n省略规则:如果原始文本中无嵌套题组,则整个 标签应被省略。\r\n没有属性,其内容将是一个或多个嵌套的 元素。\r\n注意:嵌" + + "套的 结构与顶层的 相同,可以递归包含 。\r\n5. 核心内容解析与转换规范\r\n识别与划分:\r\n优先识别大题 (),然后" + + "识别其下的子题目 ()。识别依据主要是题号、标题和分值模式。\r\n独立考察点:\r\n原则:将文本中所有可独立评分或作答的部分,尽可能地拆分" + + "为独立的 标记块。\r\n例外:对于单个逻辑题但包含多个填空/选项/判断点(如一个句子中有多个空需要填写,或一个问题下有多个判断题),请将所" + + "有相关内容合并到同一个 的 T 属性中,并提供序列化的 SA。\r\n分值提取:\r\n从原文中精确提取大题和子题的分值,并将其转换为阿拉伯数字。\r\n如果原" + + "文是百分比,务必转换为具体分数。\r\n标点规范:\r\n确保所有中文标点在输出的XML文本中统一使用全角符号。"; + + public static string RecorrectXML = "按下面的要求校验XML文本,如果有错误修正他,没有错误则直接返回,你只需要给出修正或原来的结果,不需要其他任何无" + + "关的输出, 要求:请检查这段 XML 代码的语法是否正确,包括标签的开闭、嵌套和属性的引号。请检查 XML 数据中是否存在逻辑错误或不一" + + "致的地方。XML 中的数据类型是否符合预期?例如,某个字段应该是数字却包含了文本。是否存在缺失的必需字段?XML 中的数据值是否在合理的范围内?检查" + + "属性值是否有效且完整。 XML结构为 (根元素,必需) 包含 (必需)。 (容器,必需) 包含一个或多个 (问题组,必需) 包含" + + "在 或嵌套的 内部,具有 Id (必需), T (必需), S (必需), SPM (可选) 属性,可包含子元素 (可选), (可选,包含一" + + "个或多个 ), (可选,包含一个或多个嵌套的 )。 (子题目容器,必需) 包含在 内部,包含一个或多个 (子题目,必需) 包含" + + "在 内部,具有 Id (必需), T (必需), S (必需), SPM (可选), SA (可选) 属性,可包含子元素 (可选,包含一个或多个 )。 (选项容器,必需) 包" + + "含在 内部,包含一个或多个 (选项,必需) 包含在 内部,具有 V (必需) 属性。"; + + + public static string BreakQuestions = "请识别以下文本中的每一道大题。将其转换为XML格式,你只需要给出转换后的结果,不需要其他任何无关的输出,其中 是XML的根元素。用 标记来包裹每一道大题。请确保标记后的文本保持原始格式和内容。"; + + + public static string ParseSignelQuestion = "请将以下提供的一道大题内容,转换为符合以下 C# 类结构的 XML 格式,你只需要给出转换后的结果,不需要其他任何无关的输出:" + + "XML 的根元素为 。并填充以下属性:Id:对应 QuestionGroup.Id(从大题开头的题号中提取)。T:对应 QuestionGroup.Title(" + + "从大题的标题中提取)。S:对应 QuestionGroup.Score(从大题中识别的分值)。 元素:对应 QuestionGroup.QuestionReference。**子题目" + + "(SubQuestion)**将作为 内部的 元素列表中的 元素。如果大题下有子题目,则将它们包裹在 元素中。对于每个 元素," + + "填充以下属性:Id:对应 SubQuestion.SubId(从子题号中提取)。T:对应 SubQuestion.Stem(从子题目的题干中提取)。S:对应 SubQuestion.Score" + + "(从子题目中识别的分值)。SPM:对应 SubQuestion.ScoreProblemMarker。SA:对应 SubQuestion.SampleAnswer。**选项(Option)**将作为" + + " 内部的 元素列表中的 元素。如果子题目有选项,则将它们包裹在 元素中。对于每个 元素,填充 V 属性:V:对应 Option.Value" + + "(从选项内容中提取)。嵌套题组:如果大题内部包含其他大题,则作为 内部的 元素列表中的 元素(即嵌套 )。" + + "请确保生成的 XML 严格遵循上述结构和命名约定,以方便 C# XmlSerializer 进行反序列化。"; + } +} diff --git a/TechHelper.Client/AI/AIModels.cs b/TechHelper.Client/AI/AIModels.cs new file mode 100644 index 0000000..9f5b53a --- /dev/null +++ b/TechHelper.Client/AI/AIModels.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using System.Reflection; + +namespace TechHelper.Client.AI +{ + public enum AIModelsEnum + { + [Description("glm-4v-flash")] + GLM4VFlash, + + [Description("cogview-3-flash")] + Cogview3Flash, + + [Description("cogvideox-flash")] + CogVideoXFlash, + + [Description("glm-4-flash-250414")] + GLM4Flash25, + + [Description("glm-z1-flash")] + GLMZ1Flash + + + } + + + public static class EnumExtensions + { + public static string GetDescription(this Enum value) + { + FieldInfo field = value.GetType().GetField(value.ToString()); + DescriptionAttribute attribute = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute; + return attribute == null ? value.ToString() : attribute.Description; + } + } + + public class AIModels + { + + } +} diff --git a/TechHelper.Client/AI/AiService.cs b/TechHelper.Client/AI/AiService.cs new file mode 100644 index 0000000..e8ed922 --- /dev/null +++ b/TechHelper.Client/AI/AiService.cs @@ -0,0 +1,58 @@ +using Entities.Contracts; +using Newtonsoft.Json; + +namespace TechHelper.Client.AI +{ + public class AiService : IAIService + { + private readonly GLMZ1Client _glmClient; + + public AiService() + { + _glmClient = new GLMZ1Client(AIConfiguration.APIkey); + } + + + public async Task CallGLM(string userContent, string AnsConfig, AIModelsEnum aIModels/* = AIModelsEnum.GLMZ1Flash*/) + { + string model = aIModels.GetDescription(); + var request = new ChatCompletionRequest + { + Model = model, + Messages = new List + { + new UserMessage(AnsConfig + userContent) + } + }; + + try + { + var response = await _glmClient.ChatCompletionsSync(request); + if (response?.Choices != null && response.Choices.Count > 0) + { + string content = response.Choices[0].Message?.Content; + if (!string.IsNullOrEmpty(content)) + { + // 移除 ... 标签及其内容 + int startIndex = content.IndexOf(""); + int endIndex = content.IndexOf(""); + if (startIndex != -1 && endIndex != -1 && endIndex > startIndex) + { + content = content.Remove(startIndex, endIndex - startIndex + "".Length); + } + return content.Trim(); + } + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"API 请求错误:{ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"发生未知错误:{ex.Message}"); + } + return null; + } + } +} diff --git a/TechHelper.Client/AI/GLMZ1Api.cs b/TechHelper.Client/AI/GLMZ1Api.cs new file mode 100644 index 0000000..3c82638 --- /dev/null +++ b/TechHelper.Client/AI/GLMZ1Api.cs @@ -0,0 +1,217 @@ +using Newtonsoft.Json; +using System.Text; + +namespace TechHelper.Client.AI +{ + public class Message + { + [JsonProperty("role")] + public string Role { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + } + + // 系统消息 + public class SystemMessage : Message + { + public SystemMessage(string content) + { + Role = "system"; + Content = content; + } + } + + // 用户消息 + public class UserMessage : Message + { + public UserMessage(string content) + { + Role = "user"; + Content = content; + } + } + + // 助手消息 + public class AssistantMessage : Message + { + public AssistantMessage(string content) + { + Role = "assistant"; + Content = content; + } + } + + // GLM-Z1 Chat Completions API 请求体 + public class ChatCompletionRequest + { + [JsonProperty("model")] + public string Model { get; set; } + + [JsonProperty("messages")] + public List Messages { get; set; } + + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("do_sample")] + public bool? DoSample { get; set; } + + [JsonProperty("stream")] + public bool? Stream { get; set; } + + [JsonProperty("temperature")] + public float? Temperature { get; set; } + + [JsonProperty("top_p")] + public float? TopP { get; set; } + + [JsonProperty("max_tokens")] + public int? MaxTokens { get; set; } + + [JsonProperty("stop")] + public List Stop { get; set; } + + [JsonProperty("user_id")] + public string UserId { get; set; } + } + + // GLM-Z1 Chat Completions API 响应体 + public class ChatCompletionResponse + { + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("created")] + public long Created { get; set; } + + [JsonProperty("model")] + public string Model { get; set; } + + [JsonProperty("choices")] + public List Choices { get; set; } + + [JsonProperty("usage")] + public CompletionUsage Usage { get; set; } + } + + public class CompletionChoice + { + [JsonProperty("index")] + public int Index { get; set; } + + [JsonProperty("finish_reason")] + public string FinishReason { get; set; } + + [JsonProperty("message")] + public CompletionMessage Message { get; set; } + + // 流式响应中的delta + [JsonProperty("delta")] + public CompletionMessage Delta { get; set; } + } + + public class CompletionMessage + { + [JsonProperty("role")] + public string Role { get; set; } + + [JsonProperty("content")] + public string Content { get; set; } + } + + public class CompletionUsage + { + [JsonProperty("prompt_tokens")] + public int PromptTokens { get; set; } + + [JsonProperty("completion_tokens")] + public int CompletionTokens { get; set; } + + [JsonProperty("total_tokens")] + public int TotalTokens { get; set; } + } + + // 异步调用响应 + public class AsyncTaskStatus + { + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("model")] + public string Model { get; set; } + + [JsonProperty("task_status")] + public string TaskStatus { get; set; } + } + + // 异步查询结果响应 + public class AsyncCompletion : AsyncTaskStatus + { + [JsonProperty("choices")] + public List Choices { get; set; } + + [JsonProperty("usage")] + public CompletionUsage Usage { get; set; } + } + + // GLM-Z1 API 客户端 + public class GLMZ1Client + { + private readonly HttpClient _httpClient; + private readonly string _apiKey; + private const string BaseUrl = "https://open.bigmodel.cn/api/paas/v4/"; + + public GLMZ1Client(string apiKey) + { + _apiKey = apiKey; + _httpClient = new HttpClient(); + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}"); + _httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + } + + // 同步调用 Chat Completions API + public async Task ChatCompletionsSync(ChatCompletionRequest request) + { + request.Stream = false; // 确保同步调用时 stream 为 false + var jsonContent = JsonConvert.SerializeObject(request); + var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync($"{BaseUrl}chat/completions", httpContent); + response.EnsureSuccessStatusCode(); // 确保请求成功 + + var responseString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseString); + } + + // 异步调用 Chat Completions API + public async Task ChatCompletionsAsync(ChatCompletionRequest request) + { + var jsonContent = JsonConvert.SerializeObject(request); + var httpContent = new StringContent(jsonContent, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync($"{BaseUrl}async/chat/completions", httpContent); + response.EnsureSuccessStatusCode(); + + var responseString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseString); + } + + // 查询异步任务结果 + public async Task RetrieveCompletionResult(string taskId) + { + var response = await _httpClient.GetAsync($"{BaseUrl}async-result/{taskId}"); + response.EnsureSuccessStatusCode(); + + var responseString = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(responseString); + } + + // TODO: 实现流式调用,这会涉及循环读取 HttpResponseMessage.Content.ReadAsStreamAsync() + // 并解析 SSE 事件,此处为简化暂不提供完整实现。 + // public async IAsyncEnumerable ChatCompletionsStream(ChatCompletionRequest request) { ... } + } +} diff --git a/TechHelper.Client/AI/IAIService.cs b/TechHelper.Client/AI/IAIService.cs new file mode 100644 index 0000000..b014a79 --- /dev/null +++ b/TechHelper.Client/AI/IAIService.cs @@ -0,0 +1,7 @@ +namespace TechHelper.Client.AI +{ + public interface IAIService + { + public Task CallGLM(string userContent, string AnsConfig, AIModelsEnum aIModels = AIModelsEnum.GLMZ1Flash); + } +} diff --git a/TechHelper.Client/App.razor b/TechHelper.Client/App.razor new file mode 100644 index 0000000..d07039a --- /dev/null +++ b/TechHelper.Client/App.razor @@ -0,0 +1,25 @@ + + + + + + @if (context.User.Identity?.IsAuthenticated != true) + { + + } + else + { +

You are not authorized to access this resource.

+ } +
+
+ +
+ + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/TechHelper.Client/AuthProviders/AuthStateProvider.cs b/TechHelper.Client/AuthProviders/AuthStateProvider.cs new file mode 100644 index 0000000..39e89aa --- /dev/null +++ b/TechHelper.Client/AuthProviders/AuthStateProvider.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Components.Authorization; +using System.Security.Claims; +using System.Net.Http.Headers; +using TechHelper.Features; +using Microsoft.JSInterop; + +namespace TechHelper.Client.AuthProviders +{ + public class AuthStateProvider : AuthenticationStateProvider + { + private readonly HttpClient _httpClient; + private readonly AuthenticationState _anonymous; + private readonly ILocalStorageService _localStorageService; + + public AuthStateProvider(ILocalStorageService localStorageService, HttpClient httpClient) + { + _localStorageService = localStorageService; + _httpClient = httpClient; + _anonymous = new AuthenticationState( + new ClaimsPrincipal(new ClaimsIdentity())); + } + + + public override async Task GetAuthenticationStateAsync() + { + string? token = null; + + try + { + token = _localStorageService.GetItem("authToken"); + } + catch (Exception ex) + { + Console.WriteLine($"Error accessing LocalStorage or parsing token: {ex.Message}"); + return _anonymous; + } + if (string.IsNullOrEmpty(token)) + return _anonymous; + + _httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("bearer", token); + + return new AuthenticationState(new ClaimsPrincipal( + new ClaimsIdentity(JWTParser.ParseClaimsFromJwt(token), "jwtAuthType"))); + } + + public void NotifyUserAuthentication(string token) + { + var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity( + JWTParser.ParseClaimsFromJwt(token), "jwtAuthType")); + + var authState = Task.FromResult(new AuthenticationState(authenticatedUser)); + + NotifyAuthenticationStateChanged(authState); + } + + public void NotifyUserLogout() + { + var authState = Task.FromResult(_anonymous); + + NotifyAuthenticationStateChanged(authState); + } + } +} diff --git a/TechHelper.Client/AuthProviders/TestAuthStateProvider.cs b/TechHelper.Client/AuthProviders/TestAuthStateProvider.cs new file mode 100644 index 0000000..219578e --- /dev/null +++ b/TechHelper.Client/AuthProviders/TestAuthStateProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.AspNetCore.Components.Authorization; +using System.Security.Claims; + +namespace TechHelper.Client.AuthProviders +{ + public class TestAuthStateProvider : AuthenticationStateProvider + { + + + public override async Task GetAuthenticationStateAsync() + { + var claims = new List + { + new Claim(ClaimTypes.Name, "John Doe"), + new Claim(ClaimTypes.Role, "Administrator") + }; + + var anonymous = new ClaimsIdentity(); + + return await Task.FromResult(new AuthenticationState(new ClaimsPrincipal(anonymous))); + } + } +} diff --git a/TechHelper.Client/Exam/Exam.cs b/TechHelper.Client/Exam/Exam.cs new file mode 100644 index 0000000..6e15a0d --- /dev/null +++ b/TechHelper.Client/Exam/Exam.cs @@ -0,0 +1,258 @@ +using Newtonsoft.Json; +using System.Xml.Serialization; // 确保引用此命名空间 +using System.Collections.Generic; +using System.IO; // 用于 XML 反序列化 + +namespace TechHelper.Client.Exam +{ + + [XmlRoot("EP")] + public class StringsList + { + + [XmlElement("Q")] + public List Items { get; set; } + } + + // XML 根元素 + [XmlRoot("EP")] + public class ExamPaper + { + // XML 特性: 包含 列表 + [XmlArray("QGs")] + [XmlArrayItem("QG")] + [JsonProperty("QuestionGroups")] + public List QuestionGroups { get; set; } = new List(); + } + + [XmlRoot("QG")] + public class QuestionGroup + { + // JSON 特性 + [JsonProperty("题号")] + // XML 特性:作为 属性 + [XmlAttribute("Id")] + public string Id { get; set; } + + [JsonProperty("标题")] + [XmlAttribute("T")] // T for Title + public string Title { get; set; } + + [JsonProperty("分值")] + [XmlAttribute("S")] // S for Score + public int Score { get; set; } + + [JsonProperty("分值问题标记")] + [XmlAttribute("SPM")] // SPM for ScoreProblemMarker + public string ScoreProblemMarker { get; set; } = ""; // 初始化为空字符串,避免 null + + [JsonProperty("题目引用")] + [XmlElement("QR")] // QR for QuestionReference,作为 元素 + public string QuestionReference { get; set; } = ""; // 初始化为空字符串 + + [JsonProperty("子题目")] + [XmlArray("SQs")] // SQs 包含 列表 + [XmlArrayItem("SQ")] + public List SubQuestions { get; set; } = new List(); + + [JsonProperty("子题组")] + [XmlArray("SQGs")] // SQGs 包含 列表 (嵌套题组) + [XmlArrayItem("QG")] + public List SubQuestionGroups { get; set; } = new List(); + } + + // 子题目类 + public class SubQuestion + { + [JsonProperty("子题号")] + [XmlAttribute("Id")] // Id for SubId + public string SubId { get; set; } + + [JsonProperty("题干")] + [XmlAttribute("T")] // T for Text (Stem) + public string Stem { get; set; } + + [JsonProperty("分值")] + [XmlAttribute("S")] // S for Score + public int Score { get; set; } // 分值通常为整数 + + [JsonProperty("分值问题标记")] + [XmlAttribute("SPM")] // SPM for ScoreProblemMarker + public string ScoreProblemMarker { get; set; } = ""; + + // 注意:这里的 Options 需要适配 XML 结构 + // 因此它不再是 List,而是 List