- @Html.Label("Tags", htmlAttributes: new { @class = "control-label col-md-2" })
+ @Html.Label("Tags", "Tags", htmlAttributes: new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.UserChosenTags, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.UserChosenTags, "", new { @class = "text-danger" })
@@ -60,5 +60,6 @@
@Html.Partial("PostValidation")
@section Scripts {
- @Scripts.Render("~/bundles/jqueryval")
+
+
}
diff --git a/SampleWebApp/Views/PostsAsync/Edit.cshtml b/SampleWebApp/Views/PostsAsync/Edit.cshtml
index 75b1706..22eb70e 100644
--- a/SampleWebApp/Views/PostsAsync/Edit.cshtml
+++ b/SampleWebApp/Views/PostsAsync/Edit.cshtml
@@ -41,7 +41,7 @@
- @Html.Label("Tags", htmlAttributes: new { @class = "control-label col-md-2" })
+ @Html.Label("Tags", "Tags", htmlAttributes: new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.UserChosenTags, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.UserChosenTags, "", new { @class = "text-danger" })
@@ -63,5 +63,6 @@
@Html.Partial("PostValidation")
@section Scripts {
- @Scripts.Render("~/bundles/jqueryval")
+
+
}
diff --git a/SampleWebApp/Views/PostsAsync/Index.cshtml b/SampleWebApp/Views/PostsAsync/Index.cshtml
index 706400f..e30990c 100644
--- a/SampleWebApp/Views/PostsAsync/Index.cshtml
+++ b/SampleWebApp/Views/PostsAsync/Index.cshtml
@@ -12,7 +12,7 @@
}
@if (TempData["errorMessage"] != null)
{
-
@TempData["errorMessage"]
+
@TempData["errorMessage"]
}
diff --git a/SampleWebApp/Views/Shared/Error.cshtml b/SampleWebApp/Views/Shared/Error.cshtml
index be55b17..26ebcf0 100644
--- a/SampleWebApp/Views/Shared/Error.cshtml
+++ b/SampleWebApp/Views/Shared/Error.cshtml
@@ -1,9 +1,6 @@
-@model System.Web.Mvc.HandleErrorInfo
-
-@{
+@{
ViewBag.Title = "Error";
}
Error.
An error occurred while processing your request.
-
diff --git a/SampleWebApp/Views/Shared/_Layout.cshtml b/SampleWebApp/Views/Shared/_Layout.cshtml
index 345fd91..bdc5736 100644
--- a/SampleWebApp/Views/Shared/_Layout.cshtml
+++ b/SampleWebApp/Views/Shared/_Layout.cshtml
@@ -1,11 +1,11 @@
-@using SampleWebApp.Infrastructure
-
+
@ViewBag.Title - SampleMvcWebApp
- @Styles.Render("~/Content/css")
+
+
@@ -50,11 +50,12 @@
- @Scripts.Render("~/bundles/javascript")
- @RenderSection("scripts", required: false)
+
+
+ @await RenderSectionAsync("scripts", required: false)
diff --git a/SampleWebApp/Views/Tags/Index.cshtml b/SampleWebApp/Views/Tags/Index.cshtml
index 03c2653..efd3f7b 100644
--- a/SampleWebApp/Views/Tags/Index.cshtml
+++ b/SampleWebApp/Views/Tags/Index.cshtml
@@ -12,7 +12,7 @@
}
@if (TempData["errorMessage"] != null)
{
-
@TempData["errorMessage"]
+
@TempData["errorMessage"]
}
diff --git a/SampleWebApp/Views/TagsAsync/Index.cshtml b/SampleWebApp/Views/TagsAsync/Index.cshtml
index 92c0337..e13f53e 100644
--- a/SampleWebApp/Views/TagsAsync/Index.cshtml
+++ b/SampleWebApp/Views/TagsAsync/Index.cshtml
@@ -12,7 +12,7 @@
}
@if (TempData["errorMessage"] != null)
{
-
@TempData["errorMessage"]
+
@TempData["errorMessage"]
}
@Html.ValidationSummary(false, "", new { @class = "text-danger" })
diff --git a/SampleWebApp/Views/Web.config b/SampleWebApp/Views/Web.config
deleted file mode 100644
index ba26898..0000000
--- a/SampleWebApp/Views/Web.config
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/SampleWebApp/Views/_ViewImports.cshtml b/SampleWebApp/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..d8681e7
--- /dev/null
+++ b/SampleWebApp/Views/_ViewImports.cshtml
@@ -0,0 +1,3 @@
+@using SampleWebApp
+@using SampleWebApp.Models
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/SampleWebApp/Views/_ViewStart.cshtml b/SampleWebApp/Views/_ViewStart.cshtml
index 2de6241..a5f1004 100644
--- a/SampleWebApp/Views/_ViewStart.cshtml
+++ b/SampleWebApp/Views/_ViewStart.cshtml
@@ -1,3 +1,3 @@
@{
- Layout = "~/Views/Shared/_Layout.cshtml";
+ Layout = "_Layout";
}
diff --git a/SampleWebApp/Web.AzureRelease.config b/SampleWebApp/Web.AzureRelease.config
deleted file mode 100644
index 7ea0845..0000000
--- a/SampleWebApp/Web.AzureRelease.config
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Azure
-
-
- Azure
-
-
-
-
\ No newline at end of file
diff --git a/SampleWebApp/Web.Debug.config b/SampleWebApp/Web.Debug.config
deleted file mode 100644
index 680849f..0000000
--- a/SampleWebApp/Web.Debug.config
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/SampleWebApp/Web.Release.config b/SampleWebApp/Web.Release.config
deleted file mode 100644
index 943c9c0..0000000
--- a/SampleWebApp/Web.Release.config
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/SampleWebApp/Web.WebWizRelease.config b/SampleWebApp/Web.WebWizRelease.config
deleted file mode 100644
index 451e45b..0000000
--- a/SampleWebApp/Web.WebWizRelease.config
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- WebWiz
-
-
- jonsmith_
-
-
-
-
\ No newline at end of file
diff --git a/SampleWebApp/Web.config b/SampleWebApp/Web.config
deleted file mode 100644
index 02309cc..0000000
--- a/SampleWebApp/Web.config
+++ /dev/null
@@ -1,110 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- LocalHost
-
-
- jonsmith_
-
-
-
-
\ No newline at end of file
diff --git a/SampleWebApp/appsettings.Development.json b/SampleWebApp/appsettings.Development.json
new file mode 100644
index 0000000..2cad7ee
--- /dev/null
+++ b/SampleWebApp/appsettings.Development.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "ConnectionStrings": {
+ "_comment": "Do not put credentials here. Supply the connection string via `dotnet user-secrets set ConnectionStrings:SampleWebAppDb \"...\"` or the ConnectionStrings__SampleWebAppDb environment variable. See README.md.",
+ "SampleWebAppDb": ""
+ }
+}
diff --git a/SampleWebApp/appsettings.json b/SampleWebApp/appsettings.json
new file mode 100644
index 0000000..9b94cab
--- /dev/null
+++ b/SampleWebApp/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "SampleWebAppDb": ""
+ }
+}
diff --git a/SampleWebApp/packages.config b/SampleWebApp/packages.config
deleted file mode 100644
index bc278c0..0000000
--- a/SampleWebApp/packages.config
+++ /dev/null
@@ -1,47 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/SampleWebApp/Content/Site.css b/SampleWebApp/wwwroot/css/Site.css
similarity index 100%
rename from SampleWebApp/Content/Site.css
rename to SampleWebApp/wwwroot/css/Site.css
diff --git a/SampleWebApp/Content/bootstrap-theme.css b/SampleWebApp/wwwroot/css/bootstrap-theme.css
similarity index 100%
rename from SampleWebApp/Content/bootstrap-theme.css
rename to SampleWebApp/wwwroot/css/bootstrap-theme.css
diff --git a/SampleWebApp/Content/bootstrap-theme.css.map b/SampleWebApp/wwwroot/css/bootstrap-theme.css.map
similarity index 100%
rename from SampleWebApp/Content/bootstrap-theme.css.map
rename to SampleWebApp/wwwroot/css/bootstrap-theme.css.map
diff --git a/SampleWebApp/Content/bootstrap-theme.min.css b/SampleWebApp/wwwroot/css/bootstrap-theme.min.css
similarity index 100%
rename from SampleWebApp/Content/bootstrap-theme.min.css
rename to SampleWebApp/wwwroot/css/bootstrap-theme.min.css
diff --git a/SampleWebApp/Content/bootstrap.css b/SampleWebApp/wwwroot/css/bootstrap.css
similarity index 100%
rename from SampleWebApp/Content/bootstrap.css
rename to SampleWebApp/wwwroot/css/bootstrap.css
diff --git a/SampleWebApp/Content/bootstrap.css.map b/SampleWebApp/wwwroot/css/bootstrap.css.map
similarity index 100%
rename from SampleWebApp/Content/bootstrap.css.map
rename to SampleWebApp/wwwroot/css/bootstrap.css.map
diff --git a/SampleWebApp/Content/bootstrap.min.css b/SampleWebApp/wwwroot/css/bootstrap.min.css
similarity index 100%
rename from SampleWebApp/Content/bootstrap.min.css
rename to SampleWebApp/wwwroot/css/bootstrap.min.css
diff --git a/SampleWebApp/Content/img/setup-progress.gif b/SampleWebApp/wwwroot/css/img/setup-progress.gif
similarity index 100%
rename from SampleWebApp/Content/img/setup-progress.gif
rename to SampleWebApp/wwwroot/css/img/setup-progress.gif
diff --git a/SampleWebApp/Content/img/task-progress.gif b/SampleWebApp/wwwroot/css/img/task-progress.gif
similarity index 100%
rename from SampleWebApp/Content/img/task-progress.gif
rename to SampleWebApp/wwwroot/css/img/task-progress.gif
diff --git a/SampleWebApp/favicon.ico b/SampleWebApp/wwwroot/favicon.ico
similarity index 100%
rename from SampleWebApp/favicon.ico
rename to SampleWebApp/wwwroot/favicon.ico
diff --git a/SampleWebApp/fonts/glyphicons-halflings-regular.eot b/SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.eot
similarity index 100%
rename from SampleWebApp/fonts/glyphicons-halflings-regular.eot
rename to SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.eot
diff --git a/SampleWebApp/fonts/glyphicons-halflings-regular.svg b/SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.svg
similarity index 100%
rename from SampleWebApp/fonts/glyphicons-halflings-regular.svg
rename to SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.svg
diff --git a/SampleWebApp/fonts/glyphicons-halflings-regular.ttf b/SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.ttf
similarity index 100%
rename from SampleWebApp/fonts/glyphicons-halflings-regular.ttf
rename to SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.ttf
diff --git a/SampleWebApp/fonts/glyphicons-halflings-regular.woff b/SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.woff
similarity index 100%
rename from SampleWebApp/fonts/glyphicons-halflings-regular.woff
rename to SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.woff
diff --git a/SampleWebApp/fonts/glyphicons-halflings-regular.woff2 b/SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.woff2
similarity index 100%
rename from SampleWebApp/fonts/glyphicons-halflings-regular.woff2
rename to SampleWebApp/wwwroot/fonts/glyphicons-halflings-regular.woff2
diff --git a/SampleWebApp/Scripts/bootstrap.min.js b/SampleWebApp/wwwroot/js/bootstrap.min.js
similarity index 100%
rename from SampleWebApp/Scripts/bootstrap.min.js
rename to SampleWebApp/wwwroot/js/bootstrap.min.js
diff --git a/SampleWebApp/Scripts/jquery-1.10.2.min.js b/SampleWebApp/wwwroot/js/jquery-1.10.2.min.js
similarity index 100%
rename from SampleWebApp/Scripts/jquery-1.10.2.min.js
rename to SampleWebApp/wwwroot/js/jquery-1.10.2.min.js
diff --git a/SampleWebApp/Scripts/jquery.validate.min.js b/SampleWebApp/wwwroot/js/jquery.validate.min.js
similarity index 100%
rename from SampleWebApp/Scripts/jquery.validate.min.js
rename to SampleWebApp/wwwroot/js/jquery.validate.min.js
diff --git a/SampleWebApp/Scripts/jquery.validate.unobtrusive.js b/SampleWebApp/wwwroot/js/jquery.validate.unobtrusive.js
similarity index 100%
rename from SampleWebApp/Scripts/jquery.validate.unobtrusive.js
rename to SampleWebApp/wwwroot/js/jquery.validate.unobtrusive.js
diff --git a/ServiceLayer/App.config b/ServiceLayer/App.config
deleted file mode 100644
index dac3ee5..0000000
--- a/ServiceLayer/App.config
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/ServiceLayer/BlogServices/BlogListDto.cs b/ServiceLayer/BlogServices/BlogListDto.cs
index c50f8e8..64dc1ae 100644
--- a/ServiceLayer/BlogServices/BlogListDto.cs
+++ b/ServiceLayer/BlogServices/BlogListDto.cs
@@ -26,15 +26,15 @@
#endregion
using System.ComponentModel.DataAnnotations;
-using System.Runtime.CompilerServices;
using DataLayer.DataClasses.Concrete;
-using GenericServices.Core;
-
-[assembly: InternalsVisibleTo("Tests")]
+using GenericServices;
namespace ServiceLayer.BlogServices
{
- public class BlogListDto : EfGenericDto
+ ///
+ /// Read-only list DTO for a Blog. Consume via ICrudServices.ReadManyNoTracked<BlogListDto>().
+ ///
+ public class BlogListDto : ILinkToEntity
{
[UIHint("HiddenInput")]
@@ -46,14 +46,6 @@ public class BlogListDto : EfGenericDto
[EmailAddress]
public string EmailAddress { get; set; }
- public int PostsCount { get; set; } //Uses AutoMapper Aggregate
-
- //----------------------------------------------
- //overridden properties or methods
-
- protected override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.List; }
- }
+ public int PostsCount { get; set; } //Uses AutoMapper flattening of Blog.Posts.Count
}
}
diff --git a/ServiceLayer/PostServices/DetailPostDto.cs b/ServiceLayer/PostServices/DetailPostDto.cs
index 2cdc60d..1f6d160 100644
--- a/ServiceLayer/PostServices/DetailPostDto.cs
+++ b/ServiceLayer/PostServices/DetailPostDto.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: DetailPostDto.cs
@@ -28,20 +28,22 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using System.Runtime.CompilerServices;
-using DataLayer.DataClasses;
+using AutoMapper;
using DataLayer.DataClasses.Concrete;
-using GenericLibsBase;
-using GenericLibsBase.Core;
using GenericServices;
-using GenericServices.Core;
+using GenericServices.Configuration;
using ServiceLayer.UiClasses;
-[assembly: InternalsVisibleTo("Tests")]
-
namespace ServiceLayer.PostServices
{
- public class DetailPostDto : EfGenericDto
+ ///
+ /// Create/Read/Update DTO for a Post.
+ /// Reading is done via ICrudServices.ReadSingle<DetailPostDto>(id).
+ /// The Blogger dropdown / Tags multi-select population and the mapping of the user's selection
+ /// back onto the Post entity are NOT handled by GenericServices (it has no SetupSecondaryData hook);
+ /// use for that plus the actual Create/Update of the Post.
+ ///
+ public class DetailPostDto : ILinkToEntity
{
[UIHint("HiddenInput")]
@@ -62,11 +64,10 @@ public class DetailPostDto : EfGenericDto
//properties that cannot be set directly (The data layer looks after them)
[ScaffoldColumn(false)]
- [DoNotCopyBackToDatabase]
public DateTime LastUpdated { get; set; }
//------------------------------------------
- //these two items are altered by the SetupRestOfDto method based on the user's selection
+ //these two items are altered by IPostCrudHelper based on the user's selection
[UIHint("HiddenInput")]
public int BlogId { get; set; }
@@ -75,7 +76,7 @@ public class DetailPostDto : EfGenericDto
public ICollection Tags { get; set; }
//-------------------------------------------
- //now the various lists for user interaction
+ //now the various lists for user interaction (populated by IPostCrudHelper.SetupSecondaryData)
///
/// This allows a single blogger to be chosen from the list
@@ -85,16 +86,14 @@ public class DetailPostDto : EfGenericDto
public MultiSelectListType UserChosenTags { get; set; }
//-------------------------------------------
- //calculated properties to help display
- //(Note: SampleMvcWebApp was written before calculated properties using [Computed] was added to GenericServices.
- //see https://github.com/JonPSmith/GenericServices/wiki/Calculated-properties for a better way of doing this
+ //calculated properties to help display
///
/// When it was last updated in DateTime format
///
public DateTime LastUpdatedUtc { get { return DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc); } }
- public string TagNames { get { return string.Join(", ", Tags.Select(x => x.Name)); } }
+ public string TagNames { get { return Tags == null ? string.Empty : string.Join(", ", Tags.Select(x => x.Name)); } }
//ctor
public DetailPostDto()
@@ -102,118 +101,33 @@ public DetailPostDto()
Bloggers = new DropDownListType();
UserChosenTags = new MultiSelectListType();
}
+ }
- //----------------------------------------------
- //overridden methods
-
- protected override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.AllCrud; }
- }
-
- ///
- /// This sets up the dropdownlist for the possible bloggers and the MultiSelectList of tags
- ///
- ///
- ///
- protected override void SetupSecondaryData(IGenericServicesDbContext context, DetailPostDto dto)
- {
-
- dto.Bloggers.SetupDropDownListContent(
- context.Set()
- .ToList()
- .Select(x => new KeyValuePair(x.Name, x.BlogId.ToString("D"))),
- "--- choose blogger ---");
- if (dto.PostId != 0)
- //there is an entry, so set the selected value to that
- dto.Bloggers.SetSelectedValue(dto.BlogId.ToString("D"));
-
- var preselectedTags = dto.PostId == 0
- ? new List>() //Create, so no tags selected yet
- : Tags
- .Select(x => new { Key = x.Name, Value = x.TagId })
- .ToList()
- .Select(x => new KeyValuePair(x.Key, x.Value))
- .ToList();
- dto.UserChosenTags.SetupMultiSelectList(
- context.Set().ToList().Select(x => new KeyValuePair(x.Name, x.TagId)), preselectedTags);
- }
-
- protected override ISuccessOrErrors CreateDataFromDto(IGenericServicesDbContext context, DetailPostDto source)
- {
- var status = SetupRestOfDto(context);
-
- return status.IsValid
- ? base.CreateDataFromDto(context, this)
- : SuccessOrErrors.ConvertNonResultStatus(status);
- }
-
- protected override ISuccessOrErrors UpdateDataFromDto(IGenericServicesDbContext context, DetailPostDto source, Post destination)
- {
- var status = SetupRestOfDto(context, destination);
-
- return status.IsValid
- ? base.UpdateDataFromDto(context, this, destination)
- : status;
- }
-
- //---------------------------------------------------
- //private helpers
-
- private ISuccessOrErrors SetupRestOfDto(IGenericServicesDbContext context, Post post = null)
- {
-
- var db = context as SampleWebAppDb;
- if (db == null)
- throw new NullReferenceException("The IDbContextWithValidation must be linked to TemplateWebAppDb.");
-
- var status = SuccessOrErrors.Success("OK if no errors set");
-
- //now we sort out the blogger
- var errMsg = SetBloggerIdFromDropDownList(db);
- if (errMsg != null)
- status.AddNamedParameterError("Bloggers", errMsg);
-
- //now we sort out the tags
- errMsg = ChangeTagsBasedOnMultiSelectList(db, post);
- if (errMsg != null)
- status.AddNamedParameterError("UserChosenTags", errMsg);
-
- return status;
- }
-
- private string SetBloggerIdFromDropDownList(SampleWebAppDb db)
+ ///
+ /// Tells GenericServices' AutoMapper config to ignore the UI-only helper members so the
+ /// read projection (and any save mapping) does not attempt to map them.
+ ///
+ public class DetailPostDtoConfig : PerDtoConfig
+ {
+ public override Action> AlterReadMapping
{
-
- var blogId = Bloggers.SelectedValueAsInt;
- if (blogId == null)
- return "The blogger was not selected. You must do that before the post can be saved.";
-
- var blogger = db.Blogs.Find((int)blogId);
- if (blogger == null)
- return "Could not find the blogger you selected. Did another user delete it?";
-
- BlogId = (int)blogId; //will be copied over to database entity by AutoMapper
- return null;
+ get
+ {
+ return cfg => cfg
+ .ForMember(d => d.Bloggers, o => o.Ignore())
+ .ForMember(d => d.UserChosenTags, o => o.Ignore());
+ }
}
- private string ChangeTagsBasedOnMultiSelectList(SampleWebAppDb db, Post post = null)
+ public override Action> AlterSaveMapping
{
- var requiredTagIds = UserChosenTags.GetFinalSelectionAsInts();
- if (!requiredTagIds.Any())
- return "You must select at least one tag for the post.";
-
- if (requiredTagIds.Any(x => db.Tags.Find(x) == null))
- return "Could not find one of the tags. Did another user delete it?";
-
- if (post != null)
- //This is an update so we need to load the tags
- db.Entry(post).Collection(p => p.Tags).Load();
-
- var newTagsForPost = db.Tags.Where(x => requiredTagIds.Contains(x.TagId)).ToList();
- Tags = newTagsForPost; //will be copied over to database entity by AutoMapper
-
- return null;
+ get
+ {
+ return cfg => cfg
+ .ForMember(d => d.Tags, o => o.Ignore())
+ .ForMember(d => d.Blogger, o => o.Ignore())
+ .ForMember(d => d.LastUpdated, o => o.Ignore());
+ }
}
}
}
diff --git a/ServiceLayer/PostServices/DetailPostDtoAsync.cs b/ServiceLayer/PostServices/DetailPostDtoAsync.cs
index dd27b14..bf20784 100644
--- a/ServiceLayer/PostServices/DetailPostDtoAsync.cs
+++ b/ServiceLayer/PostServices/DetailPostDtoAsync.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: DetailPostDtoAsync.cs
@@ -27,23 +27,22 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Data.Entity;
using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Threading.Tasks;
-using DataLayer.DataClasses;
+using AutoMapper;
using DataLayer.DataClasses.Concrete;
-using GenericLibsBase;
-using GenericLibsBase.Core;
using GenericServices;
-using GenericServices.Core;
+using GenericServices.Configuration;
using ServiceLayer.UiClasses;
-[assembly: InternalsVisibleTo("Tests")]
-
namespace ServiceLayer.PostServices
{
- public class DetailPostDtoAsync : EfGenericDtoAsync
+ ///
+ /// Create/Read/Update DTO for a Post used by the async path.
+ /// Reading is done via ICrudServicesAsync.ReadSingleAsync<DetailPostDtoAsync>(id).
+ /// Dropdown/multi-select setup and mapping the user's selection onto the Post entity are handled by
+ /// (GenericServices has no SetupSecondaryData hook).
+ ///
+ public class DetailPostDtoAsync : ILinkToEntity
{
[UIHint("HiddenInput")]
@@ -64,20 +63,19 @@ public class DetailPostDtoAsync : EfGenericDtoAsync
//properties that cannot be set directly (The data layer looks after them)
[ScaffoldColumn(false)]
- [DoNotCopyBackToDatabase]
public DateTime LastUpdated { get; set; }
//------------------------------------------
- //these two items are altered by the SetupRestOfDto method based on the user's selection
+ //these two items are altered by IPostCrudHelper based on the user's selection
[UIHint("HiddenInput")]
public int BlogId { get; set; }
[ScaffoldColumn(false)]
- public ICollection Tags { get; set; } //this must be copied back
+ public ICollection Tags { get; set; }
//-------------------------------------------
- //now the various lists for user interaction
+ //now the various lists for user interaction (populated by IPostCrudHelper.SetupSecondaryDataAsync)
///
/// This allows a single blogger to be chosen from the list
@@ -88,15 +86,13 @@ public class DetailPostDtoAsync : EfGenericDtoAsync
//-------------------------------------------
//calculated properties to help display
- //(Note: SampleMvcWebApp was written before calculated properties using [Computed] was added to GenericServices.
- //see https://github.com/JonPSmith/GenericServices/wiki/Calculated-properties for a better way of doing this
///
/// When it was last updated in DateTime format
///
public DateTime LastUpdatedUtc { get { return DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc); } }
- public string TagNames { get { return string.Join(", ", Tags.Select(x => x.Name)); } }
+ public string TagNames { get { return Tags == null ? string.Empty : string.Join(", ", Tags.Select(x => x.Name)); } }
//ctor
public DetailPostDtoAsync()
@@ -104,120 +100,33 @@ public DetailPostDtoAsync()
Bloggers = new DropDownListType();
UserChosenTags = new MultiSelectListType();
}
+ }
-
- //----------------------------------------------
- //overridden methods
-
- protected override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.AllCrud; }
- }
-
- ///
- /// This sets up the dropdownlist for the possible bloggers and the MultiSelectList of tags
- ///
- ///
- ///
- protected override async Task SetupSecondaryDataAsync(IGenericServicesDbContext context, DetailPostDtoAsync dto)
- {
-
- var bloggers = await context.Set().ToListAsync();
-
- dto.Bloggers.SetupDropDownListContent(bloggers.Select(x => new KeyValuePair(x.Name, x.BlogId.ToString("D"))),
- "--- choose blogger ---");
- if (dto.PostId != 0)
- //there is an entry, so set the selected value to that
- dto.Bloggers.SetSelectedValue(dto.BlogId.ToString("D"));
-
- var preselectedTags = dto.PostId == 0
- ? new List>() //Create, so no tags selected yet
- : Tags
- .Select(x => new { Key = x.Name, Value = x.TagId })
- .ToList()
- .Select(x => new KeyValuePair(x.Key, x.Value))
- .ToList();
- dto.UserChosenTags.SetupMultiSelectList(
- context.Set().ToList().Select(x => new KeyValuePair(x.Name, x.TagId)), preselectedTags);
- }
-
-
- protected override async Task> CreateDataFromDtoAsync(IGenericServicesDbContext context, DetailPostDtoAsync source)
- {
- var status = await SetupRestOfDto(context);
-
- return status.IsValid
- ? await base.CreateDataFromDtoAsync(context, this)
- : SuccessOrErrors.ConvertNonResultStatus(status);
- }
-
- protected override async Task UpdateDataFromDtoAsync(IGenericServicesDbContext context, DetailPostDtoAsync source, Post destination)
- {
- var status = await SetupRestOfDto(context, destination);
-
- return status.IsValid
- ? await base.UpdateDataFromDtoAsync(context, this, destination)
- : status;
- }
-
- private async Task SetupRestOfDto(IGenericServicesDbContext context, Post post = null)
- {
-
- var db = context as SampleWebAppDb;
- if (db == null)
- throw new NullReferenceException("The IDbContextWithValidation must be linked to TemplateWebAppDb.");
-
- var status = SuccessOrErrors.Success("OK if no errors set");
-
- //now we sort out the blogger
- var errMsg = await SetupBloggerIdFromDropDownList(db, post);
- if (errMsg != null)
- status.AddNamedParameterError("Bloggers", errMsg);
-
- //now we sort out the tags
- errMsg = await ChangeTagsBasedOnMultiSelectList(db, post);
- if (errMsg != null)
- status.AddNamedParameterError("UserChosenTags", errMsg);
-
- return status;
- }
-
-
- //---------------------------------------------------
- //private helpers
-
- private async Task SetupBloggerIdFromDropDownList(SampleWebAppDb db, Post post)
+ ///
+ /// Tells GenericServices' AutoMapper config to ignore the UI-only helper members so the
+ /// read projection (and any save mapping) does not attempt to map them.
+ ///
+ public class DetailPostDtoAsyncConfig : PerDtoConfig
+ {
+ public override Action> AlterReadMapping
{
-
- var blogId = Bloggers.SelectedValueAsInt;
- if (blogId == null)
- return "The blogger was not selected. You must do that before the post can be saved.";
-
- var blogger = await db.Blogs.FindAsync((int)blogId);
- if (blogger == null)
- return "Could not find the blogger you selected. Did another user delete it?";
-
- BlogId = (int)blogId;
- return null;
+ get
+ {
+ return cfg => cfg
+ .ForMember(d => d.Bloggers, o => o.Ignore())
+ .ForMember(d => d.UserChosenTags, o => o.Ignore());
+ }
}
- private async Task ChangeTagsBasedOnMultiSelectList(SampleWebAppDb db, Post post)
+ public override Action> AlterSaveMapping
{
- var requiredTagIds = UserChosenTags.GetFinalSelectionAsInts();
- if (!requiredTagIds.Any())
- return "You must select at least one tag for the post.";
-
- if (requiredTagIds.Any(x => db.Tags.Find(x) == null))
- return "Could not find one of the tags. Did another user delete it?";
-
- if (post != null)
- //This is an update so we need to load the tags
- db.Entry(post).Collection(p => p.Tags).Load();
-
- var newTagsForPost = await db.Tags.Where(x => requiredTagIds.Contains(x.TagId)).ToListAsync();
- Tags = newTagsForPost; //will be copied over by copyDtoToData
-
- return null;
+ get
+ {
+ return cfg => cfg
+ .ForMember(d => d.Tags, o => o.Ignore())
+ .ForMember(d => d.Blogger, o => o.Ignore())
+ .ForMember(d => d.LastUpdated, o => o.Ignore());
+ }
}
}
}
diff --git a/ServiceLayer/PostServices/PostCrudHelper.cs b/ServiceLayer/PostServices/PostCrudHelper.cs
new file mode 100644
index 0000000..13f9289
--- /dev/null
+++ b/ServiceLayer/PostServices/PostCrudHelper.cs
@@ -0,0 +1,294 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: PostCrudHelper.cs
+// Date Created: 2014/08/16
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Threading.Tasks;
+using DataLayer.DataClasses;
+using DataLayer.DataClasses.Concrete;
+using Microsoft.EntityFrameworkCore;
+using ServiceLayer.UiClasses;
+using StatusGeneric;
+
+namespace ServiceLayer.PostServices
+{
+ ///
+ /// Hand-written service that replaces the EF6-GenericServices "secondary data" hooks
+ /// (SetupSecondaryData, CreateDataFromDto, UpdateDataFromDto) that
+ /// EfCore.GenericServices does not provide. It:
+ ///
+ /// - populates the Blogger dropdown and Tags multi-select on a Post detail DTO, and
+ /// - creates/updates the Post entity from that DTO, resolving the selected blogger and tags.
+ ///
+ /// Read operations (list/detail projections) are still done through ICrudServices in the web app.
+ ///
+ public interface IPostCrudHelper
+ {
+ /// Populates and .
+ /// Call before rendering the Create/Edit views and on redisplay after a validation error.
+ void SetupSecondaryData(DetailPostDto dto);
+
+ /// Async version of for .
+ Task SetupSecondaryDataAsync(DetailPostDtoAsync dto);
+
+ /// Creates a new Post from the DTO. Returns a status carrying any validation errors.
+ IStatusGeneric CreatePost(DetailPostDto dto);
+
+ /// Async version of .
+ Task CreatePostAsync(DetailPostDtoAsync dto);
+
+ /// Updates the Post identified by dto.PostId from the DTO. Returns a status carrying any errors.
+ IStatusGeneric UpdatePost(DetailPostDto dto);
+
+ /// Async version of .
+ Task UpdatePostAsync(DetailPostDtoAsync dto);
+ }
+
+ public class PostCrudHelper : IPostCrudHelper
+ {
+ private readonly SampleWebAppDb _db;
+
+ public PostCrudHelper(SampleWebAppDb db)
+ {
+ _db = db;
+ }
+
+ //--------------------------------------------------
+ //Secondary data (dropdown + multi-select) population
+
+ public void SetupSecondaryData(DetailPostDto dto)
+ {
+ var blogs = _db.Blogs.ToList();
+ var tags = _db.Tags.ToList();
+ PopulateSecondaryData(dto.Bloggers, dto.UserChosenTags, blogs, tags,
+ dto.PostId, dto.BlogId, dto.Tags);
+ }
+
+ public async Task SetupSecondaryDataAsync(DetailPostDtoAsync dto)
+ {
+ var blogs = await _db.Blogs.ToListAsync();
+ var tags = await _db.Tags.ToListAsync();
+ PopulateSecondaryData(dto.Bloggers, dto.UserChosenTags, blogs, tags,
+ dto.PostId, dto.BlogId, dto.Tags);
+ }
+
+ //--------------------------------------------------
+ //Create
+
+ public IStatusGeneric CreatePost(DetailPostDto dto)
+ {
+ var status = BuildAndSave(dto.PostId, dto.Title, dto.Content,
+ dto.Bloggers, dto.UserChosenTags, isUpdate: false);
+ return status;
+ }
+
+ public Task CreatePostAsync(DetailPostDtoAsync dto)
+ {
+ return BuildAndSaveAsync(dto.PostId, dto.Title, dto.Content,
+ dto.Bloggers, dto.UserChosenTags, isUpdate: false);
+ }
+
+ //--------------------------------------------------
+ //Update
+
+ public IStatusGeneric UpdatePost(DetailPostDto dto)
+ {
+ return BuildAndSave(dto.PostId, dto.Title, dto.Content,
+ dto.Bloggers, dto.UserChosenTags, isUpdate: true);
+ }
+
+ public Task UpdatePostAsync(DetailPostDtoAsync dto)
+ {
+ return BuildAndSaveAsync(dto.PostId, dto.Title, dto.Content,
+ dto.Bloggers, dto.UserChosenTags, isUpdate: true);
+ }
+
+ //--------------------------------------------------
+ //private helpers
+
+ private static void PopulateSecondaryData(DropDownListType bloggers, MultiSelectListType userChosenTags,
+ IReadOnlyCollection allBlogs, IReadOnlyCollection allTags,
+ int postId, int selectedBlogId, ICollection selectedTags)
+ {
+ bloggers.SetupDropDownListContent(
+ allBlogs.Select(x => new KeyValuePair(x.Name, x.BlogId.ToString("D"))),
+ "--- choose blogger ---");
+ if (postId != 0)
+ bloggers.SetSelectedValue(selectedBlogId.ToString("D"));
+
+ var preselectedTags = postId == 0 || selectedTags == null
+ ? new List>()
+ : selectedTags
+ .Select(x => new KeyValuePair(x.Name, x.TagId))
+ .ToList();
+ userChosenTags.SetupMultiSelectList(
+ allTags.Select(x => new KeyValuePair(x.Name, x.TagId)), preselectedTags);
+ }
+
+ private IStatusGeneric BuildAndSave(int postId, string title, string content,
+ DropDownListType bloggers, MultiSelectListType userChosenTags, bool isUpdate)
+ {
+ var status = new StatusGenericHandler();
+
+ var post = ResolvePost(postId, isUpdate, status);
+ if (status.HasErrors)
+ return status;
+
+ var blogId = ResolveBlogId(bloggers, status);
+ var tags = ResolveTags(userChosenTags, status);
+ if (status.HasErrors)
+ return status;
+
+ ApplyToPost(post, title, content, blogId.Value, tags, isUpdate);
+
+ status.CombineStatuses(ValidateEntity(post));
+ if (status.HasErrors)
+ return status;
+
+ if (!isUpdate)
+ _db.Posts.Add(post);
+ _db.SaveChanges();
+ status.Message = isUpdate ? "Successfully updated the post." : "Successfully created the post.";
+ return status;
+ }
+
+ private async Task BuildAndSaveAsync(int postId, string title, string content,
+ DropDownListType bloggers, MultiSelectListType userChosenTags, bool isUpdate)
+ {
+ var status = new StatusGenericHandler();
+
+ var post = isUpdate
+ ? await _db.Posts.Include(p => p.Tags).SingleOrDefaultAsync(p => p.PostId == postId)
+ : new Post();
+ if (isUpdate && post == null)
+ {
+ status.AddError("Could not find the post you asked for. Did another user delete it?");
+ return status;
+ }
+
+ var blogId = ResolveBlogId(bloggers, status);
+ var tags = await ResolveTagsAsync(userChosenTags, status);
+ if (status.HasErrors)
+ return status;
+
+ ApplyToPost(post, title, content, blogId.Value, tags, isUpdate);
+
+ status.CombineStatuses(ValidateEntity(post));
+ if (status.HasErrors)
+ return status;
+
+ if (!isUpdate)
+ _db.Posts.Add(post);
+ await _db.SaveChangesAsync();
+ status.Message = isUpdate ? "Successfully updated the post." : "Successfully created the post.";
+ return status;
+ }
+
+ ///
+ /// Returns the Post to write to. A newly created Post is deliberately left untracked until
+ /// validation has passed, so a failed create cannot leave an Added entity in the request's
+ /// change tracker for a later SaveChanges to persist.
+ ///
+ private Post ResolvePost(int postId, bool isUpdate, StatusGenericHandler status)
+ {
+ if (!isUpdate)
+ return new Post();
+
+ var post = _db.Posts.Include(p => p.Tags).SingleOrDefault(p => p.PostId == postId);
+ if (post == null)
+ status.AddError("Could not find the post you asked for. Did another user delete it?");
+ return post;
+ }
+
+ private int? ResolveBlogId(DropDownListType bloggers, StatusGenericHandler status)
+ {
+ var blogId = bloggers.SelectedValueAsInt;
+ if (blogId == null)
+ {
+ status.AddError("The blogger was not selected. You must do that before the post can be saved.", nameof(DetailPostDto.Bloggers));
+ return null;
+ }
+ if (_db.Blogs.Find(blogId.Value) == null)
+ {
+ status.AddError("Could not find the blogger you selected. Did another user delete it?", nameof(DetailPostDto.Bloggers));
+ return null;
+ }
+ return blogId;
+ }
+
+ private List ResolveTags(MultiSelectListType userChosenTags, StatusGenericHandler status)
+ {
+ var requiredTagIds = userChosenTags.GetFinalSelectionAsInts();
+ if (!requiredTagIds.Any())
+ {
+ status.AddError("You must select at least one tag for the post.", nameof(DetailPostDto.UserChosenTags));
+ return new List();
+ }
+ var tags = _db.Tags.Where(x => requiredTagIds.Contains(x.TagId)).ToList();
+ if (tags.Count != requiredTagIds.Length)
+ status.AddError("Could not find one of the tags. Did another user delete it?", nameof(DetailPostDto.UserChosenTags));
+ return tags;
+ }
+
+ private async Task> ResolveTagsAsync(MultiSelectListType userChosenTags, StatusGenericHandler status)
+ {
+ var requiredTagIds = userChosenTags.GetFinalSelectionAsInts();
+ if (!requiredTagIds.Any())
+ {
+ status.AddError("You must select at least one tag for the post.", nameof(DetailPostDtoAsync.UserChosenTags));
+ return new List();
+ }
+ var tags = await _db.Tags.Where(x => requiredTagIds.Contains(x.TagId)).ToListAsync();
+ if (tags.Count != requiredTagIds.Length)
+ status.AddError("Could not find one of the tags. Did another user delete it?", nameof(DetailPostDtoAsync.UserChosenTags));
+ return tags;
+ }
+
+ private static void ApplyToPost(Post post, string title, string content, int blogId, List tags, bool isUpdate)
+ {
+ post.Title = title;
+ post.Content = content;
+ post.BlogId = blogId;
+ if (post.Tags == null)
+ post.Tags = new List();
+ else
+ post.Tags.Clear();
+ foreach (var tag in tags)
+ post.Tags.Add(tag);
+ }
+
+ private static IStatusGeneric ValidateEntity(Post post)
+ {
+ var status = new StatusGenericHandler();
+ var results = new List();
+ var context = new ValidationContext(post);
+ if (!Validator.TryValidateObject(post, context, results, validateAllProperties: true))
+ status.AddValidationResults(results);
+ return status;
+ }
+ }
+}
diff --git a/ServiceLayer/PostServices/SimplePostDto.cs b/ServiceLayer/PostServices/SimplePostDto.cs
index 3a664bc..f44bd63 100644
--- a/ServiceLayer/PostServices/SimplePostDto.cs
+++ b/ServiceLayer/PostServices/SimplePostDto.cs
@@ -28,16 +28,15 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using System.Runtime.CompilerServices;
using DataLayer.DataClasses.Concrete;
using GenericServices;
-using GenericServices.Core;
-
-[assembly: InternalsVisibleTo("Tests")]
namespace ServiceLayer.PostServices
{
- public class SimplePostDto : EfGenericDto
+ ///
+ /// Read-only list DTO for a Post. Consume via ICrudServices.ReadManyNoTracked<SimplePostDto>().
+ ///
+ public class SimplePostDto : ILinkToEntity
{
[UIHint("HiddenInput")]
@@ -63,14 +62,6 @@ public class SimplePostDto : EfGenericDto
///
public DateTime LastUpdatedUtc { get { return DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc); } }
- public string TagNames { get { return string.Join(", ", Tags.Select(x => x.Name)); } }
-
- //----------------------------------------------
- //overridden properties or methods
-
- protected override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.List; }
- }
+ public string TagNames { get { return Tags == null ? string.Empty : string.Join(", ", Tags.Select(x => x.Name)); } }
}
}
diff --git a/ServiceLayer/PostServices/SimplePostDtoAsync.cs b/ServiceLayer/PostServices/SimplePostDtoAsync.cs
index f19d434..bb661fd 100644
--- a/ServiceLayer/PostServices/SimplePostDtoAsync.cs
+++ b/ServiceLayer/PostServices/SimplePostDtoAsync.cs
@@ -28,15 +28,16 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
-using System.Runtime.CompilerServices;
using DataLayer.DataClasses.Concrete;
-using GenericServices.Core;
-
-[assembly: InternalsVisibleTo("Tests")]
+using GenericServices;
namespace ServiceLayer.PostServices
{
- public class SimplePostDtoAsync : EfGenericDtoAsync
+ ///
+ /// Read-only list DTO for a Post used by the async path. Consume via
+ /// ICrudServicesAsync.ReadManyNoTracked<SimplePostDtoAsync>().
+ ///
+ public class SimplePostDtoAsync : ILinkToEntity
{
[UIHint("HiddenInput")]
@@ -59,14 +60,6 @@ public class SimplePostDtoAsync : EfGenericDtoAsync
///
public DateTime LastUpdatedUtc { get { return DateTime.SpecifyKind(LastUpdated, DateTimeKind.Utc); } }
- public string TagNames { get { return string.Join(", ", Tags.Select(x => x.Name)); } }
-
- //----------------------------------------------
- //overridden properties or methods
-
- protected override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.List; }
- }
+ public string TagNames { get { return Tags == null ? string.Empty : string.Join(", ", Tags.Select(x => x.Name)); } }
}
}
diff --git a/ServiceLayer/Properties/AssemblyInfo.cs b/ServiceLayer/Properties/AssemblyInfo.cs
deleted file mode 100644
index 2793a90..0000000
--- a/ServiceLayer/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: AssemblyInfo.cs
-// Date Created: 2014/05/20
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("ServiceLayer")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("ServiceLayer")]
-[assembly: AssemblyCopyright("Copyright © 2014")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("35097f07-8bb1-4da4-ab04-7b4ac4894e18")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/ServiceLayer/ServiceLayer.csproj b/ServiceLayer/ServiceLayer.csproj
index 09244c2..2317bff 100644
--- a/ServiceLayer/ServiceLayer.csproj
+++ b/ServiceLayer/ServiceLayer.csproj
@@ -1,136 +1,27 @@
-
-
-
+
+
- Debug
- AnyCPU
- {D2813927-0F38-43C3-B47C-AE8F00D50CAE}
- Library
- Properties
+ net10.0
+ disable
+ disable
+ false
ServiceLayer
ServiceLayer
- v4.5.1
- 512
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
- bin\ReleaseAzure\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
-
-
- bin\AzureRelease\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
-
-
- bin\WebWizRelease\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
-
-
-
- False
- ..\packages\Autofac.3.5.0\lib\net40\Autofac.dll
-
-
- ..\packages\AutoMapper.4.2.1\lib\net45\AutoMapper.dll
- True
-
-
- ..\packages\DelegateDecompiler.0.18.0\lib\net40-Client\DelegateDecompiler.dll
- True
-
-
- ..\packages\DelegateDecompiler.EntityFramework.0.18.0\lib\net45\DelegateDecompiler.EntityFramework.dll
- True
-
-
- ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll
- True
-
-
- ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll
- True
-
-
- ..\packages\GenericLibsBase.1.0.1\lib\GenericLibsBase.dll
- True
-
-
- ..\packages\GenericServices.1.0.9\lib\GenericServices.dll
- True
-
-
- ..\packages\Mono.Reflection.1.0.0.0\lib\Mono.Reflection.dll
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
+
+
+
+
-
+
-
- {264e1878-12de-4099-b8d7-cc53a73fea49}
- DataLayer
-
+
-
-
-
\ No newline at end of file
+
+
diff --git a/ServiceLayer/Startup/ServiceLayerModule.cs b/ServiceLayer/Startup/ServiceLayerModule.cs
deleted file mode 100644
index 39b40ef..0000000
--- a/ServiceLayer/Startup/ServiceLayerModule.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: ServiceLayerModule.cs
-// Date Created: 2014/05/20
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-
-using Autofac;
-using DataLayer.Startup;
-using GenericServices;
-
-namespace ServiceLayer.Startup
-{
- public class ServiceLayerModule : Module
- {
-
- ///
- /// This registers all items in service layer and below
- ///
- ///
- protected override void Load(ContainerBuilder builder)
- {
-
- //Now register the DataLayer
- builder.RegisterModule(new DataLayerModule());
-
- //---------------------------
- //Register service layer: autowire all
- builder.RegisterAssemblyTypes(GetType().Assembly).AsImplementedInterfaces();
-
- //and register the GenericServices assembly
- builder.RegisterAssemblyTypes(typeof(IListService).Assembly).AsImplementedInterfaces();
-
- }
-
- }
-}
diff --git a/ServiceLayer/Startup/ServiceLayerServiceCollectionExtensions.cs b/ServiceLayer/Startup/ServiceLayerServiceCollectionExtensions.cs
new file mode 100644
index 0000000..66a05dd
--- /dev/null
+++ b/ServiceLayer/Startup/ServiceLayerServiceCollectionExtensions.cs
@@ -0,0 +1,79 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: ServiceLayerServiceCollectionExtensions.cs
+// Date Created: 2014/05/20
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System.Reflection;
+using DataLayer.DataClasses;
+using DataLayer.Startup;
+using GenericServices.Configuration;
+using GenericServices.Setup;
+using Microsoft.Extensions.DependencyInjection;
+using ServiceLayer.PostServices;
+using StatusGeneric;
+
+namespace ServiceLayer.Startup
+{
+ ///
+ /// Replaces the old Autofac ServiceLayerModule. Registers GenericServices
+ /// (ICrudServices / ICrudServicesAsync + the DTO↔entity AutoMapper config) and the
+ /// service-layer's own hand-written services (e.g. ).
+ ///
+ public static class ServiceLayerServiceCollectionExtensions
+ {
+ ///
+ /// Registers the service layer (and the data layer beneath it). The host must still register the
+ /// context via AddDbContext<SampleWebAppDb>(...).
+ ///
+ public static IServiceCollection AddServiceLayer(this IServiceCollection services)
+ {
+ services.AddDataLayer();
+
+ //Runs just before every GenericServices SaveChanges. Reports duplicate Tag.Slug values as
+ //validation errors (which controllers copy into ModelState) instead of letting the data layer's
+ //ValidationException escape as an unhandled 500. This is the GenericServices replacement for the
+ //EF6 ValidateEntity hook.
+ var config = new GenericServicesConfig
+ {
+ BeforeSaveChanges = context =>
+ {
+ var status = new StatusGenericHandler();
+ if (context is SampleWebAppDb sampleDb)
+ foreach (var error in sampleDb.GetSlugUniquenessErrors())
+ status.AddError(error);
+ return status;
+ }
+ };
+
+ //Registers ICrudServices/ICrudServicesAsync and builds the AutoMapper mappings by scanning
+ //this assembly for ILinkToEntity DTOs and PerDtoConfig<,> classes.
+ services.GenericServicesSimpleSetup(config, Assembly.GetExecutingAssembly());
+
+ //Hand-written helper that replaces the DTO's old SetupSecondaryData/Create/Update hooks.
+ services.AddScoped();
+
+ return services;
+ }
+ }
+}
diff --git a/ServiceLayer/TagServices/TagListDto.cs b/ServiceLayer/TagServices/TagListDto.cs
index efd0496..1c9070b 100644
--- a/ServiceLayer/TagServices/TagListDto.cs
+++ b/ServiceLayer/TagServices/TagListDto.cs
@@ -26,15 +26,15 @@
#endregion
using System.ComponentModel.DataAnnotations;
-using System.Runtime.CompilerServices;
using DataLayer.DataClasses.Concrete;
-using GenericServices.Core;
-
-[assembly: InternalsVisibleTo("Tests")]
+using GenericServices;
namespace ServiceLayer.TagServices
{
- public class TagListDto : EfGenericDto
+ ///
+ /// Read-only list DTO for a Tag. Consume via ICrudServices.ReadManyNoTracked<TagListDto>().
+ ///
+ public class TagListDto : ILinkToEntity
{
[UIHint("HiddenInput")]
@@ -45,14 +45,6 @@ public class TagListDto : EfGenericDto
public string Slug { get; set; }
- public int PostsCount { get; set; } //uses AutoMapper Aggregate
-
- //----------------------------------------------
- //overridden properties or methods
-
- protected override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.List; }
- }
+ public int PostsCount { get; set; } //Uses AutoMapper flattening of Tag.Posts.Count
}
}
diff --git a/ServiceLayer/packages.config b/ServiceLayer/packages.config
deleted file mode 100644
index 9c06c52..0000000
--- a/ServiceLayer/packages.config
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Tests/App.config b/Tests/App.config
deleted file mode 100644
index 875486b..0000000
--- a/Tests/App.config
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- jonsmith_
-
-
-
-
\ No newline at end of file
diff --git a/Tests/Helpers/DbSnapShot.cs b/Tests/Helpers/DbSnapShot.cs
index a28d446..b399b10 100644
--- a/Tests/Helpers/DbSnapShot.cs
+++ b/Tests/Helpers/DbSnapShot.cs
@@ -43,7 +43,9 @@ public class DbSnapShot
public DbSnapShot(SampleWebAppDb db)
{
NumBlogs = db.Blogs.Count();
- NumPostTagLinks = db.Database.SqlQuery("SELECT COUNT(*) FROM dbo.TagPosts").First();
+ //The Post<->Tag many-to-many join table is named "PostTag" by EF Core. There is no
+ //db.Database.SqlQuery in EF Core the way EF6 had it, so count the join rows via the model.
+ NumPostTagLinks = db.Posts.SelectMany(p => p.Tags).Count();
NumPosts = db.Posts.Count();
NumTags = db.Tags.Count();
}
diff --git a/Tests/Helpers/DummyIDbContextWithValidation.cs b/Tests/Helpers/DummyIDbContextWithValidation.cs
deleted file mode 100644
index dd1544c..0000000
--- a/Tests/Helpers/DummyIDbContextWithValidation.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: DummyIDbContextWithValidation.cs
-// Date Created: 2014/06/26
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System;
-using System.Collections.Generic;
-using System.Data.Entity;
-using System.Data.Entity.Infrastructure;
-using System.Data.Entity.Validation;
-using System.Threading.Tasks;
-using GenericServices;
-using GenericServices.Core;
-
-namespace Tests.Helpers
-{
- public class DummyIDbContextWithValidation : IGenericServicesDbContext
- {
-
- public bool SaveChangesWithValidationCalled { get; private set; }
-
-
- public DbSet Set() where TEntity : class
- {
- throw new NotImplementedException();
- }
-
- public DbSet Set(Type entityType)
- {
- throw new NotImplementedException();
- }
-
- public int SaveChanges()
- {
- SaveChangesWithValidationCalled = true;
- return 1;
- }
-
- public async Task SaveChangesAsync()
- {
- SaveChangesWithValidationCalled = true;
- return 1;
- }
-
- public IEnumerable GetValidationErrors()
- {
- throw new NotImplementedException();
- }
-
- public DbEntityEntry Entry(TEntity entity) where TEntity : class
- {
- throw new NotImplementedException();
- }
-
- public DbEntityEntry Entry(object entity)
- {
- throw new NotImplementedException();
- }
-
- public void Dispose()
- {
- }
- }
-}
diff --git a/Tests/Helpers/ExtendAsserts.cs b/Tests/Helpers/ExtendAsserts.cs
index 8e85c05..d4c3fb3 100644
--- a/Tests/Helpers/ExtendAsserts.cs
+++ b/Tests/Helpers/ExtendAsserts.cs
@@ -27,7 +27,7 @@
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
-using NUnit.Framework;
+using NUnit.Framework.Legacy;
namespace Tests.Helpers
{
@@ -35,7 +35,7 @@ internal static class ExtendAsserts
{
internal static void ShouldEqual(this string actualValue, string expectedValue, string errorMessage = null)
{
- Assert.AreEqual(expectedValue, actualValue, errorMessage);
+ ClassicAssert.AreEqual(expectedValue, actualValue, errorMessage);
}
internal static void ShouldStartWith(this string actualValue, string expectedValue, string errorMessage = null)
@@ -55,57 +55,57 @@ internal static void ShouldContain(this string actualValue, string expectedValue
internal static void ShouldNotEqual(this string actualValue, string expectedValue, string errorMessage = null)
{
- Assert.True(expectedValue != actualValue, errorMessage);
+ ClassicAssert.True(expectedValue != actualValue, errorMessage);
}
internal static void ShouldEqualWithTolerance(this float actualValue, double expectedValue, double tolerance, string errorMessage = null)
{
- Assert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
+ ClassicAssert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
}
internal static void ShouldEqualWithTolerance(this long actualValue, long expectedValue, int tolerance, string errorMessage = null)
{
- Assert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
+ ClassicAssert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
}
internal static void ShouldEqualWithTolerance(this double actualValue, double expectedValue, double tolerance, string errorMessage = null)
{
- Assert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
+ ClassicAssert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
}
internal static void ShouldEqualWithTolerance(this int actualValue, int expectedValue, int tolerance, string errorMessage = null)
{
- Assert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
+ ClassicAssert.AreEqual(expectedValue, actualValue, tolerance, errorMessage);
}
internal static void ShouldEqual( this T actualValue, T expectedValue, string errorMessage = null)
{
- Assert.AreEqual(expectedValue, actualValue, errorMessage);
+ ClassicAssert.AreEqual(expectedValue, actualValue, errorMessage);
}
internal static void ShouldEqual(this T actualValue, T expectedValue, IEnumerable errorMessages)
{
- Assert.AreEqual(expectedValue, actualValue, string.Join("\n", errorMessages));
+ ClassicAssert.AreEqual(expectedValue, actualValue, string.Join("\n", errorMessages));
}
internal static void ShouldEqual(this T actualValue, T expectedValue, IEnumerable validationResults)
{
- Assert.AreEqual(expectedValue, actualValue, string.Join("\n", validationResults.Select( x => x.ErrorMessage)));
+ ClassicAssert.AreEqual(expectedValue, actualValue, string.Join("\n", validationResults.Select( x => x.ErrorMessage)));
}
internal static void ShouldNotEqual(this T actualValue, T unexpectedValue, string errorMessage = null)
{
- Assert.AreNotEqual(unexpectedValue, actualValue);
+ ClassicAssert.AreNotEqual(unexpectedValue, actualValue);
}
internal static void ShouldNotEqualNull(this T actualValue, string errorMessage = null) where T : class
{
- Assert.NotNull( actualValue);
+ ClassicAssert.NotNull( actualValue);
}
internal static void IsA(this object actualValue, string errorMessage = null)
{
- Assert.True(actualValue.GetType() == typeof(T));
+ ClassicAssert.True(actualValue.GetType() == typeof(T));
}
}
}
diff --git a/Tests/Helpers/JsonHelper.cs b/Tests/Helpers/JsonHelper.cs
deleted file mode 100644
index 2bf617f..0000000
--- a/Tests/Helpers/JsonHelper.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: JsonHelper.cs
-// Date Created: 2014/05/31
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Linq.Expressions;
-using System.Reflection;
-using System.Web.Helpers;
-using Newtonsoft.Json;
-using NUnit.Framework;
-
-namespace Tests.Helpers
-{
- static class JsonHelper
- {
-
- //public static string SerialiseToJsonUsingJsonNet(this object data)
- //{
- // JsonConvert.SerializeObject(data);
- //
- //}
-
- //public static string SerialiseToJsonIndentedUsingJsonNet(this object data)
- //{
- // return JsonConvert.SerializeObject(data, Formatting.Indented);
- //}
-
- public static string SerialiseToJson(this object data)
- {
- return Json.Encode(data);
- }
-
- public static string AssertJsonPropertyPresentAndReturnValue
- (this string jsonString,
- TSource source, Expression> propertyLambda)
- {
- return jsonString.AssertJsonPropertyInOrOutAndReturnValue(source, propertyLambda, false);
- }
-
- public static void AssertJsonPropertyNotPresent
- (this string jsonString,
- TSource source, Expression> propertyLambda)
- {
- jsonString.AssertJsonPropertyInOrOutAndReturnValue(source, propertyLambda, true);
- }
-
- //-----------------------------------------------------------------
-
- private static string AssertJsonPropertyInOrOutAndReturnValue
- (this string jsonString,
- TSource source, Expression> propertyLambda,
- bool doesNotContain)
- {
-
- var stringToFind = string.Format("\"{0}\":", source.GetPropertyInfo(propertyLambda).Name);
- var startIndex = jsonString.IndexOf(stringToFind, StringComparison.InvariantCultureIgnoreCase);
- if (doesNotContain)
- {
- if (startIndex == -1) return null; //all good
- Assert.Fail("The property '{0}' should NOT be in the in json string", stringToFind);
- }
-
- //otherwise we expect it to be in
- if (startIndex == -1)
- Assert.Fail("Looked for '{0}' in json and could not find it", stringToFind);
-
- //now return value after it
-
- var closingIndex = jsonString.IndexOf('\n', startIndex + 1);
- if (closingIndex == -1)
- throw new ValidationException("This only works on indented json, and this doesn't seem to be indented");
-
- var result = jsonString.Substring(startIndex + stringToFind.Length, closingIndex - startIndex - stringToFind.Length).Trim();
- return result.EndsWith(",") ? result.Substring(0, result.Length - 1).Trim() : result;
- }
-
- public static PropertyInfo GetPropertyInfo(
- this TSource source,
- Expression> propertyLambda)
- {
- Type type = typeof(TSource);
-
- MemberExpression member = propertyLambda.Body as MemberExpression;
- if (member == null)
- throw new ArgumentException(string.Format(
- "Expression '{0}' refers to a method, not a property.",
- propertyLambda.ToString()));
-
- PropertyInfo propInfo = member.Member as PropertyInfo;
- if (propInfo == null)
- throw new ArgumentException(string.Format(
- "Expression '{0}' refers to a field, not a property.",
- propertyLambda.ToString()));
-
- if (type != propInfo.ReflectedType &&
- !type.IsSubclassOf(propInfo.ReflectedType))
- throw new ArgumentException(string.Format(
- "Expresion '{0}' refers to a property that is not from type {1}.",
- propertyLambda.ToString(),
- type));
-
- return propInfo;
- }
-
- }
-}
diff --git a/Tests/Helpers/ModelStateTester.cs b/Tests/Helpers/ModelStateTester.cs
index 8f3068b..58dc8ab 100644
--- a/Tests/Helpers/ModelStateTester.cs
+++ b/Tests/Helpers/ModelStateTester.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: ModelStateTester.cs
@@ -25,14 +25,13 @@
// SOFTWARE.
#endregion
using System.Collections.Generic;
-using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
-using System.Globalization;
-using System.Web.Mvc;
+using System.Linq;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Tests.Helpers
{
- static class ModelStateTester
+ public static class ModelStateTester
{
public class TestModel : IValidatableObject
@@ -67,36 +66,53 @@ public TestModel(string myString, int myInt, bool createValidationError)
}
}
-
- private class TestController : Controller
- {
- public ActionResult ValidDateTestModel(TestModel model)
- {
- // ReSharper disable once Mvc.ViewNotResolved
- return View(model);
- }
- }
-
+ ///
+ /// The old version drove System.Web.Mvc's DefaultModelBinder (which no longer exists). This rebuilds the
+ /// same result the ASP.NET Core MVC validation pipeline produces: the property-level DataAnnotations are
+ /// validated first and, only if there are no property errors, the object-level IValidatableObject.Validate
+ /// is run. Errors are written into an ASP.NET Core keyed by member name,
+ /// with top-level (no member) errors under the "" key.
+ ///
public static ModelStateDictionary ReturnModelState(this TestModel model)
{
- var testController = new TestController();
+ var modelState = new ModelStateDictionary();
- var modelBinder = new ModelBindingContext()
+ var hasPropertyErrors = false;
+ foreach (var property in model.GetType().GetProperties())
{
- ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(
- () => model, model.GetType()),
- ValueProvider = new NameValueCollectionValueProvider(
- new NameValueCollection(), CultureInfo.InvariantCulture)
- };
- var binder = new DefaultModelBinder().BindModel(
- new ControllerContext(), modelBinder);
- testController.ModelState.Clear();
- testController.ModelState.Merge(modelBinder.ModelState);
+ var value = property.GetValue(model);
+ var context = new ValidationContext(model) { MemberName = property.Name };
- var viewResult = (ViewResult) testController.ValidDateTestModel(model);
- return viewResult.ViewData.ModelState;
- }
+ //Each DataAnnotations attribute is evaluated independently (as the old MVC validators did).
+ //Validator.TryValidateProperty is not used because it short-circuits after a failing
+ //RequiredAttribute, which would hide the other attribute errors the tests expect.
+ foreach (var attribute in property.GetCustomAttributes(true).OfType())
+ {
+ var result = attribute.GetValidationResult(value, context);
+ if (result != ValidationResult.Success)
+ {
+ hasPropertyErrors = true;
+ modelState.AddModelError(property.Name, result.ErrorMessage);
+ }
+ }
+ }
+ //ASP.NET Core (like the old MVC binder) only runs IValidatableObject.Validate when there are no
+ //property-level attribute errors.
+ if (!hasPropertyErrors)
+ {
+ foreach (var result in model.Validate(new ValidationContext(model)))
+ {
+ var members = result.MemberNames?.ToList() ?? new List();
+ if (members.Count == 0)
+ modelState.AddModelError("", result.ErrorMessage);
+ else
+ foreach (var member in members)
+ modelState.AddModelError(member, result.ErrorMessage);
+ }
+ }
+ return modelState;
+ }
}
}
diff --git a/Tests/Helpers/SimpleTagDto.cs b/Tests/Helpers/SimpleTagDto.cs
deleted file mode 100644
index 62d4dc8..0000000
--- a/Tests/Helpers/SimpleTagDto.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: SimpleTagDto.cs
-// Date Created: 2014/06/26
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System.ComponentModel.DataAnnotations;
-using DataLayer.DataClasses.Concrete;
-using GenericServices.Core;
-
-namespace Tests.Helpers
-{
- class SimpleTagDto : InstrumentedEfGenericDto
- {
-
- [Key]
- public int TagId { get; set; }
-
- [MaxLength(64)]
- [Required]
- [RegularExpression(@"\w*", ErrorMessage = "The slug must not contain spaces or non-alphanumeric characters.")]
- public string Slug { get; set; }
-
- [MaxLength(128)]
- [Required]
- public string Name { get; set; }
-
- //--------------------------------------
-
- protected internal override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.AllCrud; }
- }
-
- }
-}
diff --git a/Tests/Helpers/SimpleTagDtoAsync.cs b/Tests/Helpers/SimpleTagDtoAsync.cs
deleted file mode 100644
index 60256b4..0000000
--- a/Tests/Helpers/SimpleTagDtoAsync.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: SimpleTagDtoAsync.cs
-// Date Created: 2014/06/26
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System.ComponentModel.DataAnnotations;
-using DataLayer.DataClasses.Concrete;
-using GenericServices.Core;
-
-namespace Tests.Helpers
-{
- class SimpleTagDtoAsync : InstrumentedEfGenericDtoAsync
- {
-
-
- [Key]
- public int TagId { get; set; }
-
- [MaxLength(64)]
- [Required]
- [RegularExpression(@"\w*", ErrorMessage = "The slug must not contain spaces or non-alphanumeric characters.")]
- public string Slug { get; set; }
-
- [MaxLength(128)]
- [Required]
- public string Name { get; set; }
-
-
- //--------------------------------------
-
-
- protected internal override CrudFunctions SupportedFunctions
- {
- get { return CrudFunctions.AllCrud; }
- }
-
- }
-}
diff --git a/Tests/Helpers/TestDbContext.cs b/Tests/Helpers/TestDbContext.cs
new file mode 100644
index 0000000..72bbbc7
--- /dev/null
+++ b/Tests/Helpers/TestDbContext.cs
@@ -0,0 +1,68 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: TestDbContext.cs
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using DataLayer.DataClasses;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+
+namespace Tests.Helpers
+{
+ ///
+ /// Builds instances backed by an in-memory SQLite database.
+ /// The relational SQLite provider is used (rather than the EF Core in-memory provider) because it
+ /// honours the schema, including the unique index on Tag.Slug that the Slug tests rely on.
+ ///
+ /// A SQLite in-memory database only lives for as long as its connection is open, so the connection
+ /// is created and kept open by the test; several contexts can be built
+ /// on the same connection to mimic the old "new SampleWebAppDb()" pattern (a fresh change tracker
+ /// over the same underlying data).
+ ///
+ public static class TestDbContext
+ {
+ ///
+ /// Opens a new in-memory SQLite connection and creates the schema on it.
+ /// Keep the returned connection open for the lifetime of the test and dispose it when done.
+ ///
+ public static SqliteConnection CreateOpenConnection()
+ {
+ var connection = new SqliteConnection("DataSource=:memory:");
+ connection.Open();
+ using (var db = CreateContext(connection))
+ db.Database.EnsureCreated();
+ return connection;
+ }
+
+ ///
+ /// Builds a over an already-open connection.
+ ///
+ public static SampleWebAppDb CreateContext(SqliteConnection connection)
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseSqlite(connection)
+ .Options;
+ return new SampleWebAppDb(options);
+ }
+ }
+}
diff --git a/Tests/Helpers/TestFileHelpers.cs b/Tests/Helpers/TestFileHelpers.cs
deleted file mode 100644
index bb2080b..0000000
--- a/Tests/Helpers/TestFileHelpers.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: TestFileHelpers.cs
-// Date Created: 2014/06/27
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System;
-using System.IO;
-
-namespace Tests.Helpers
-{
- internal static class TestFileHelpers
- {
- private const string TestFileDirectoryName = @"\TestData";
-
- //-------------------------------------------------------------------
-
- internal static string GetTestFileFilePath(string searchPattern)
- {
- string[] fileList = GetTestFileFilesOfGivenName(searchPattern);
-
- if (fileList.Length != 1)
- throw new Exception(string.Format("GetTestFileFilePath: The searchString {0} found {1} file. Either not there or ambiguous",
- searchPattern, fileList.Length));
-
- return fileList[0];
- }
-
- internal static string GetTestFileContent(string searchPattern)
- {
- var filePath = GetTestFileFilePath(searchPattern);
- return File.ReadAllText(filePath);
- }
-
- internal static string[] GetTestFileFilesOfGivenName(string searchPattern = "")
- {
- var directory = GetTestDataFileDirectory();
- if (searchPattern.Contains(@"\"))
- {
- //Has subdirectory in search pattern, so change directory
- directory = Path.Combine(directory, searchPattern.Substring(0, searchPattern.LastIndexOf('\\')));
- searchPattern = searchPattern.Substring(searchPattern.LastIndexOf('\\')+1);
- }
-
- string[] fileList = Directory.GetFiles(directory, searchPattern);
-
- return fileList;
- }
-
-
- //------------------------------------------------------------------------------
-
- public static string GetTestDataFileDirectory(string alternateTestDir = TestFileDirectoryName)
- {
- string pathToManipulate = Environment.CurrentDirectory;
- const string debugEnding = @"\bin\debug";
- const string releaseEnding = @"\bin\release";
-
- if (pathToManipulate.EndsWith(debugEnding, StringComparison.InvariantCultureIgnoreCase))
- return pathToManipulate.Substring(0, pathToManipulate.Length - debugEnding.Length) + alternateTestDir;
- if (pathToManipulate.EndsWith(releaseEnding, StringComparison.InvariantCultureIgnoreCase))
- return pathToManipulate.Substring(0, pathToManipulate.Length - releaseEnding.Length) + alternateTestDir;
-
- throw new Exception("bad news guys. Not the expected path");
-
- }
-
- public static string GetSolutionDirectory()
- {
- string pathToManipulate = Environment.CurrentDirectory;
- const string debugEnding = @"\bin\debug";
- const string releaseEnding = @"\bin\release";
-
- string projectDir = null;
- if (pathToManipulate.EndsWith(debugEnding, StringComparison.InvariantCultureIgnoreCase))
- projectDir = pathToManipulate.Substring(0, pathToManipulate.Length - debugEnding.Length);
- if (pathToManipulate.EndsWith(releaseEnding, StringComparison.InvariantCultureIgnoreCase))
- projectDir = pathToManipulate.Substring(0, pathToManipulate.Length - releaseEnding.Length);
-
- if (projectDir == null)
- throw new Exception("bad news guys. Not the expected path");
-
- return projectDir.Substring(0, projectDir.LastIndexOf("\\", StringComparison.Ordinal));
-
- }
-
- }
-}
diff --git a/Tests/Properties/AssemblyInfo.cs b/Tests/Properties/AssemblyInfo.cs
deleted file mode 100644
index b722a8d..0000000
--- a/Tests/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: AssemblyInfo.cs
-// Date Created: 2014/05/20
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Tests")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Tests")]
-[assembly: AssemblyCopyright("Copyright © 2014")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("edff866f-292e-46db-9cdf-74c70d23322d")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Tests/Properties/Settings.Designer.cs b/Tests/Properties/Settings.Designer.cs
deleted file mode 100644
index f2bf076..0000000
--- a/Tests/Properties/Settings.Designer.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-//------------------------------------------------------------------------------
-//
-// This code was generated by a tool.
-// Runtime Version:4.0.30319.18444
-//
-// Changes to this file may cause incorrect behavior and will be lost if
-// the code is regenerated.
-//
-//------------------------------------------------------------------------------
-
-namespace Tests.Properties {
-
-
- [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")]
- internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
-
- private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
-
- public static Settings Default {
- get {
- return defaultInstance;
- }
- }
-
- [global::System.Configuration.UserScopedSettingAttribute()]
- [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [global::System.Configuration.DefaultSettingValueAttribute("jonsmith_")]
- public string DatabaseLoginPrefix {
- get {
- return ((string)(this["DatabaseLoginPrefix"]));
- }
- set {
- this["DatabaseLoginPrefix"] = value;
- }
- }
- }
-}
diff --git a/Tests/Properties/Settings.settings b/Tests/Properties/Settings.settings
deleted file mode 100644
index 74f592a..0000000
--- a/Tests/Properties/Settings.settings
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
- jonsmith_
-
-
-
\ No newline at end of file
diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj
index 5dca393..2202d9d 100644
--- a/Tests/Tests.csproj
+++ b/Tests/Tests.csproj
@@ -1,231 +1,34 @@
-
-
-
+
+
- Debug
- AnyCPU
- {6D9E7904-B2AC-49E3-83A7-6B48876F46B9}
- Library
- Properties
+ net10.0
+ disable
+ disable
+ false
Tests
Tests
- v4.5.1
- 512
+ false
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
- bin\ReleaseAzure\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
-
-
- bin\AzureRelease\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
-
-
- bin\WebWizRelease\
- TRACE
- true
- pdbonly
- AnyCPU
- prompt
- MinimumRecommendedRules.ruleset
-
-
-
- False
- ..\packages\Autofac.3.5.0\lib\net40\Autofac.dll
-
-
- ..\packages\AutoMapper.4.2.1\lib\net45\AutoMapper.dll
- True
-
-
- ..\packages\DelegateDecompiler.0.18.0\lib\net40-Client\DelegateDecompiler.dll
- True
-
-
- ..\packages\DelegateDecompiler.EntityFramework.0.18.0\lib\net45\DelegateDecompiler.EntityFramework.dll
- True
-
-
- ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll
- True
-
-
- ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll
- True
-
-
- ..\packages\GenericLibsBase.1.0.1\lib\GenericLibsBase.dll
- True
-
-
- ..\packages\GenericServices.1.0.9\lib\GenericServices.dll
- True
-
-
- False
- ..\packages\log4net.2.0.3\lib\net40-full\log4net.dll
-
-
- False
- ..\packages\Microsoft.AspNet.SignalR.Core.2.0.3\lib\net45\Microsoft.AspNet.SignalR.Core.dll
-
-
- False
- ..\packages\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll
-
-
- False
- ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll
-
-
- True
- ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll
-
-
- ..\packages\Mono.Reflection.1.0.0.0\lib\Mono.Reflection.dll
-
-
- ..\packages\Moq.4.2.1408.0717\lib\net40\Moq.dll
-
-
- False
- ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll
-
-
- ..\packages\NUnit.2.6.3\lib\nunit.framework.dll
-
-
- False
- ..\packages\Owin.1.0\lib\net40\Owin.dll
-
-
-
-
-
-
- ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll
- True
-
-
- ..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll
- True
-
-
- ..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll
- True
-
-
- ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll
- True
-
-
- ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll
- True
-
-
- ..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll
- True
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- True
- True
- Settings.settings
-
-
-
-
-
-
-
-
-
-
- Designer
-
-
-
- SettingsSingleFileGenerator
- Settings.Designer.cs
-
-
+
-
- {264e1878-12de-4099-b8d7-cc53a73fea49}
- DataLayer
-
-
- {cffee5e0-3b99-46e0-9a82-2e74621c17c5}
- SampleWebApp
-
-
- {d2813927-0f38-43c3-b47c-ae8f00d50cae}
- ServiceLayer
-
+
+
-
+
+
+
+
+
+
+
+
-
-
+
+
+
+
-
-
-
\ No newline at end of file
+
+
diff --git a/Tests/UnitTests/Group01DataLayer/Test10SetupBlogs.cs b/Tests/UnitTests/Group01DataLayer/Test10SetupBlogs.cs
index 7a03ba0..9f9b1c4 100644
--- a/Tests/UnitTests/Group01DataLayer/Test10SetupBlogs.cs
+++ b/Tests/UnitTests/Group01DataLayer/Test10SetupBlogs.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: Test10SetupBlogs.cs
@@ -26,7 +26,6 @@
#endregion
using System;
using System.Linq;
-using DataLayer.DataClasses;
using DataLayer.Startup;
using DataLayer.Startup.Internal;
using NUnit.Framework;
@@ -34,7 +33,7 @@
namespace Tests.UnitTests.Group01DataLayer
{
- class Test10SetupBlogs
+ public class Test10SetupBlogs
{
[Test]
public void Check01XmlFileLoadOk()
@@ -69,10 +68,10 @@ public void Check02XmlFileLoadBad()
[Test]
public void Check10BlogsResetSmallOk()
{
- using (var db = new SampleWebAppDb())
+ using (var connection = TestDbContext.CreateOpenConnection())
+ using (var db = TestDbContext.CreateContext(connection))
{
//SETUP
- DataLayerInitialise.InitialiseThis(false, true);
//ATTEMPT
DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
@@ -87,10 +86,10 @@ public void Check10BlogsResetSmallOk()
[Test]
public void Check11BlogsResetMediumOk()
{
- using (var db = new SampleWebAppDb())
+ using (var connection = TestDbContext.CreateOpenConnection())
+ using (var db = TestDbContext.CreateContext(connection))
{
//SETUP
- DataLayerInitialise.InitialiseThis(false, true);
//ATTEMPT
DataLayerInitialise.ResetBlogs(db, TestDataSelection.Medium);
@@ -105,13 +104,16 @@ public void Check11BlogsResetMediumOk()
//---------------------------------------------------------
[Test]
- public void Check20NullInitialiserOk()
+ public void Check20ResetBlogsIsRepeatableOk()
{
- Check10BlogsResetSmallOk(); //we call this to ensure the database is setup
- using (var db = new SampleWebAppDb())
+ //The old test toggled the EF6 "null database initialiser" (Database.SetInitializer) which no longer
+ //exists under EF Core. The behaviour worth preserving is that ResetBlogs can be run repeatedly against
+ //an already-seeded database (it deletes then reseeds) and leaves the same counts each time.
+ using (var connection = TestDbContext.CreateOpenConnection())
+ using (var db = TestDbContext.CreateContext(connection))
{
//SETUP
- DataLayerInitialise.InitialiseThis(false, false); //select null initialiser
+ DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
//ATTEMPT
DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
diff --git a/Tests/UnitTests/Group01DataLayer/Test13Validation.cs b/Tests/UnitTests/Group01DataLayer/Test13Validation.cs
index 5bba1c2..df1779b 100644
--- a/Tests/UnitTests/Group01DataLayer/Test13Validation.cs
+++ b/Tests/UnitTests/Group01DataLayer/Test13Validation.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: Test13Validation.cs
@@ -26,72 +26,92 @@
#endregion
using System;
using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
using System.Linq;
-using DataLayer.DataClasses;
using DataLayer.DataClasses.Concrete;
using DataLayer.Startup;
-using GenericServices;
+using Microsoft.Data.Sqlite;
using NUnit.Framework;
using Tests.Helpers;
namespace Tests.UnitTests.Group01DataLayer
{
- class Test13Validation
+ public class Test13Validation
{
+ private SqliteConnection _connection;
- [TestFixtureSetUp]
- public void SetUpFixture()
+ [SetUp]
+ public void SetUp()
{
- using (var db = new SampleWebAppDb())
- {
- DataLayerInitialise.InitialiseThis(false, true);
+ _connection = TestDbContext.CreateOpenConnection();
+ using (var db = TestDbContext.CreateContext(_connection))
DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
- }
}
+ [TearDown]
+ public void TearDown()
+ {
+ _connection?.Dispose();
+ }
+
+ //--------------------------------------------------------------------
+ //Tag.Slug uniqueness is now enforced by SampleWebAppDb.SaveChanges (CheckForUniqueSlugs),
+ //which throws a ValidationException on a duplicate Slug. The old GenericServices
+ //SaveChangesWithChecking()/SuccessOrErrors status object no longer exists.
+
[Test]
public void Check01ValidateTagOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
//ATTEMPT
- var dupTag = new Tag { Name = "non-duplicate slug", Slug = Guid.NewGuid().ToString("N") };
- db.Tags.Add(dupTag);
- var status = db.SaveChangesWithChecking();
+ var newTag = new Tag { Name = "non-duplicate slug", Slug = Guid.NewGuid().ToString("N") };
+ db.Tags.Add(newTag);
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
- snap.CheckSnapShot(db, 0,0,0,1);
+ snap.CheckSnapShot(db, 0, 0, 0, 1);
}
}
[Test]
public void Check02ValidateTagError()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var existingTag = db.Tags.First();
//ATTEMPT
- var dupTag = new Tag {Name = "duplicate slug", Slug = existingTag.Slug};
+ var dupTag = new Tag { Name = "duplicate slug", Slug = existingTag.Slug };
db.Tags.Add(dupTag);
- var status = db.SaveChangesWithChecking();;
+ var ex = Assert.Throws(() => db.SaveChanges());
//VERIFY
- status.IsValid.ShouldEqual(false);
- status.Errors.Count.ShouldEqual(1);
- status.Errors[0].ErrorMessage.ShouldEqual("The Slug on tag 'duplicate slug' must be unique and is already being used.");
+ ex.Message.ShouldEqual("The Slug on tag 'duplicate slug' must be unique and is already being used.");
}
}
+ //--------------------------------------------------------------------
+ //Post content/title rules live on Post as DataAnnotations + IValidatableObject. Under EF Core the
+ //DbContext no longer runs entity validation on SaveChanges (that moved to MVC ModelState / the
+ //PostCrudHelper), so these tests assert the Post's own validation via Validator.TryValidateObject,
+ //which is exactly the rule-set the removed SaveChangesWithChecking() used to enforce.
+
+ private static IList ValidatePost(Post post)
+ {
+ var results = new List();
+ Validator.TryValidateObject(post, new ValidationContext(post), results, true);
+ return results;
+ }
+
[Test]
public void Check10ValidatePostOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -106,125 +126,91 @@ public void Check10ValidatePostOk()
Content = "Nothing special",
Tags = new[] { existingTag }
};
+ var errors = ValidatePost(newPost);
db.Posts.Add(newPost);
- var status = db.SaveChangesWithChecking();;
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
- snap.CheckSnapShot(db,1,1);
+ errors.Count.ShouldEqual(0);
+ snap.CheckSnapShot(db, 1, 1);
}
}
-
[Test]
public void Check15ValidatePostTitleError()
{
- using (var db = new SampleWebAppDb())
+ //SETUP
+ var newPost = new Post()
{
- //SETUP
- var existingTag = db.Tags.First();
- var existingBlogger = db.Blogs.First();
+ Title = "Test post!",
+ Content = "Nothing special",
+ Tags = new[] { new Tag() }
+ };
- //ATTEMPT
- var newPost = new Post()
- {
- Blogger = existingBlogger,
- Title = "Test post!",
- Content = "Nothing special",
- Tags = new[] { existingTag }
- };
- db.Posts.Add(newPost);
- var status = db.SaveChangesWithChecking();;
+ //ATTEMPT
+ var errors = ValidatePost(newPost);
- //VERIFY
- status.IsValid.ShouldEqual(false);
- status.Errors.Count.ShouldEqual(1);
- status.Errors[0].ErrorMessage.ShouldEqual("Sorry, but you can't get too excited and include a ! in the title.");
- }
+ //VERIFY
+ errors.Count.ShouldEqual(1);
+ errors[0].ErrorMessage.ShouldEqual("Sorry, but you can't get too excited and include a ! in the title.");
}
[Test]
public void Check16ValidatePostTitleError()
{
- using (var db = new SampleWebAppDb())
+ //SETUP
+ var newPost = new Post()
{
- //SETUP
- var existingTag = db.Tags.First();
- var existingBlogger = db.Blogs.First();
+ Title = "Test post?",
+ Content = "Nothing special",
+ Tags = new[] { new Tag() }
+ };
- //ATTEMPT
- var newPost = new Post()
- {
- Blogger = existingBlogger,
- Title = "Test post?",
- Content = "Nothing special",
- Tags = new[] { existingTag }
- };
- db.Posts.Add(newPost);
- var status = db.SaveChangesWithChecking();;
+ //ATTEMPT
+ var errors = ValidatePost(newPost);
- //VERIFY
- status.IsValid.ShouldEqual(false);
- status.Errors.Count.ShouldEqual(1);
- status.Errors[0].ErrorMessage.ShouldEqual("Sorry, but you can't ask a question, i.e. the title can't end with '?'.");
- }
+ //VERIFY
+ errors.Count.ShouldEqual(1);
+ errors[0].ErrorMessage.ShouldEqual("Sorry, but you can't ask a question, i.e. the title can't end with '?'.");
}
[Test]
public void Check20ValidatePostContentOneError()
{
- using (var db = new SampleWebAppDb())
+ //SETUP
+ var newPost = new Post()
{
- //SETUP
- var existingTag = db.Tags.First();
- var existingBlogger = db.Blogs.First();
+ Title = "Test post",
+ Content = "Should not end sentence with sheep.",
+ Tags = new[] { new Tag() }
+ };
- //ATTEMPT
- var newPost = new Post()
- {
- Blogger = existingBlogger,
- Title = "Test post",
- Content = "Should not end sentence with sheep.",
- Tags = new[] { existingTag }
- };
- db.Posts.Add(newPost);
- var status = db.SaveChangesWithChecking();;
+ //ATTEMPT
+ var errors = ValidatePost(newPost);
- //VERIFY
- status.IsValid.ShouldEqual(false);
- status.Errors.Count.ShouldEqual(1);
- status.Errors[0].ErrorMessage.ShouldEqual("Sorry. Not allowed to end a sentance with 'sheep'.");
- }
+ //VERIFY
+ errors.Count.ShouldEqual(1);
+ errors[0].ErrorMessage.ShouldEqual("Sorry. Not allowed to end a sentance with 'sheep'.");
}
-
[Test]
public void Check21ValidatePostContentTwoErrors()
{
- using (var db = new SampleWebAppDb())
+ //SETUP
+ var newPost = new Post()
{
- //SETUP
- var existingTag = db.Tags.First();
- var existingBlogger = db.Blogs.First();
-
- //ATTEMPT
- var newPost = new Post()
- {
- Blogger = existingBlogger,
- Title = "Test post",
- Content = "Should not end sentence with sheep. Nor end sentence with lamb.",
- Tags = new[] { existingTag }
- };
- db.Posts.Add(newPost);
- var status = db.SaveChangesWithChecking();;
-
- //VERIFY
- status.IsValid.ShouldEqual(false);
- status.Errors.Count.ShouldEqual(2);
- status.Errors[0].ErrorMessage.ShouldEqual("Sorry. Not allowed to end a sentance with 'sheep'.");
- status.Errors[1].ErrorMessage.ShouldEqual("Sorry. Not allowed to end a sentance with 'lamb'.");
- }
+ Title = "Test post",
+ Content = "Should not end sentence with sheep. Nor end sentence with lamb.",
+ Tags = new[] { new Tag() }
+ };
+
+ //ATTEMPT
+ var errors = ValidatePost(newPost);
+
+ //VERIFY
+ errors.Count.ShouldEqual(2);
+ errors[0].ErrorMessage.ShouldEqual("Sorry. Not allowed to end a sentance with 'sheep'.");
+ errors[1].ErrorMessage.ShouldEqual("Sorry. Not allowed to end a sentance with 'lamb'.");
}
}
}
-
diff --git a/Tests/UnitTests/Group01DataLayer/Test14ReadWriteBlogs.cs b/Tests/UnitTests/Group01DataLayer/Test14ReadWriteBlogs.cs
index 9c70554..e738077 100644
--- a/Tests/UnitTests/Group01DataLayer/Test14ReadWriteBlogs.cs
+++ b/Tests/UnitTests/Group01DataLayer/Test14ReadWriteBlogs.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: Test14ReadWriteBlogs.cs
@@ -25,35 +25,40 @@
// SOFTWARE.
#endregion
using System;
-using System.Data.Entity;
using System.Linq;
using System.Threading;
-using DataLayer.DataClasses;
using DataLayer.DataClasses.Concrete;
using DataLayer.Startup;
-using GenericServices;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Tests.Helpers;
namespace Tests.UnitTests.Group01DataLayer
{
- class Test14ReadWriteBlogs
+ public class Test14ReadWriteBlogs
{
+ private SqliteConnection _connection;
[SetUp]
public void SetUp()
{
- using (var db = new SampleWebAppDb())
- {
- DataLayerInitialise.InitialiseThis(false, true);
+ _connection = TestDbContext.CreateOpenConnection();
+ using (var db = TestDbContext.CreateContext(_connection))
DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
- }
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ _connection?.Dispose();
}
[Test]
public void Check01ReadBlogsNoPostsOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
@@ -62,6 +67,7 @@ public void Check01ReadBlogsNoPostsOk()
//VERIFY
blogs.Count.ShouldEqual(2);
+ //EF Core does not lazy-load, so a navigation that was not Included stays null
blogs.All(x => x.Posts == null).ShouldEqual(true);
}
}
@@ -69,7 +75,7 @@ public void Check01ReadBlogsNoPostsOk()
[Test]
public void Check02ReadBlogsWithPostsOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
@@ -86,12 +92,12 @@ public void Check02ReadBlogsWithPostsOk()
[Test]
public void Check03ReadBlogsWithPostTagsOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
//ATTEMPT
- var blogs = db.Blogs.Include(x => x.Posts.Select(y => y.Tags)).ToList();
+ var blogs = db.Blogs.Include(x => x.Posts).ThenInclude(y => y.Tags).ToList();
//VERIFY
blogs.Count.ShouldEqual(2);
@@ -104,7 +110,7 @@ public void Check03ReadBlogsWithPostTagsOk()
[Test]
public void Check05ReadPostsOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
@@ -113,7 +119,9 @@ public void Check05ReadPostsOk()
//VERIFY
posts.Count.ShouldEqual(3);
- posts.All(x => x.Blogger != null).ShouldEqual(true);
+ //Under EF6 the virtual Blogger was lazy-loaded; EF Core has no lazy loading configured,
+ //so neither the Blogger nor the Tags navigation is populated without an explicit Include.
+ posts.All(x => x.Blogger == null).ShouldEqual(true);
posts.All(x => x.Tags == null).ShouldEqual(true);
}
}
@@ -122,7 +130,7 @@ public void Check05ReadPostsOk()
[Test]
public void Check06ReadPostsWithTagsOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
@@ -131,7 +139,8 @@ public void Check06ReadPostsWithTagsOk()
//VERIFY
posts.Count.ShouldEqual(3);
- posts.All(x => x.Blogger != null).ShouldEqual(true);
+ //Tags were Included so they are loaded; Blogger was not Included and (no lazy loading) stays null
+ posts.All(x => x.Blogger == null).ShouldEqual(true);
posts.All(x => x.Tags != null).ShouldEqual(true);
}
}
@@ -139,7 +148,7 @@ public void Check06ReadPostsWithTagsOk()
[Test]
public void Check10ReadTAllocatedTagsWithUglySlugOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
@@ -159,7 +168,7 @@ public void Check10ReadTAllocatedTagsWithUglySlugOk()
[Test]
public void Check20AddPostOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -176,10 +185,9 @@ public void Check20AddPostOk()
};
db.Posts.Add(newPost);
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db, 1, 1);
var uglyPosts = db.Tags.Include(x => x.Posts).Single(y => y.Slug == "ugly").Posts;
uglyPosts.Count.ShouldEqual(3);
@@ -189,7 +197,7 @@ public void Check20AddPostOk()
[Test]
public void Check21CheckUpdateSimpleOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -198,10 +206,9 @@ public void Check21CheckUpdateSimpleOk()
//ATTEMPT
var firstPost = db.Posts.First();
firstPost.Title = newGuid;
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db);
db.Posts.First().Title.ShouldEqual(newGuid);
}
@@ -211,7 +218,7 @@ public void Check21CheckUpdateSimpleOk()
[Test]
public void Check22CheckUpdateLastUpdatedOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -221,19 +228,18 @@ public void Check22CheckUpdateLastUpdatedOk()
//ATTEMPT
firstPost.Title = Guid.NewGuid().ToString();
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db);
- Assert.GreaterOrEqual(db.Posts.First().LastUpdated.Subtract(originalDateTime).Milliseconds, 400);
+ ClassicAssert.GreaterOrEqual(db.Posts.First().LastUpdated.Subtract(originalDateTime).TotalMilliseconds, 400);
}
}
[Test]
public void Check25UpdatePostToAddTagOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -243,12 +249,11 @@ public void Check25UpdatePostToAddTagOk()
//ATTEMPT
db.Entry(firstPost).Collection(x => x.Tags).Load();
firstPost.Tags.Add(badTag);
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db, 0, 1);
- firstPost = db.Blogs.Include(x => x.Posts.Select(y => y.Tags)).First().Posts.First();
+ firstPost = db.Blogs.Include(x => x.Posts).ThenInclude(y => y.Tags).First().Posts.First();
firstPost.Tags.Count.ShouldEqual(3);
}
}
@@ -256,7 +261,7 @@ public void Check25UpdatePostToAddTagOk()
[Test]
public void Check26ReplaceTagsOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -267,12 +272,11 @@ public void Check26ReplaceTagsOk()
db.Entry(firstPost).Collection(x => x.Tags).Load();
firstPost.Tags = tagsNotInFirstPostTracked;
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db, 0, -1);
- firstPost = db.Blogs.Include(x => x.Posts.Select(y => y.Tags)).First().Posts.First();
+ firstPost = db.Blogs.Include(x => x.Posts).ThenInclude(y => y.Tags).First().Posts.First();
firstPost.Tags.Count.ShouldEqual(1);
}
}
@@ -280,7 +284,7 @@ public void Check26ReplaceTagsOk()
[Test]
public void Check30CheckCreateLastUpdatedOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
@@ -289,43 +293,46 @@ public void Check30CheckCreateLastUpdatedOk()
Thread.Sleep(400);
//ATTEMPT
+ //Reset the PK so EF Core treats this detached copy as a brand-new row (EF6 ignored the
+ //store-generated key on Add; EF Core would otherwise try to insert the explicit PostId).
+ firstPostUntracked.PostId = 0;
firstPostUntracked.Title = Guid.NewGuid().ToString();
firstPostUntracked.Blogger = db.Blogs.First();
firstPostUntracked.Tags = db.Tags.Take(2).ToList();
db.Posts.Add(firstPostUntracked);
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db,1,2);
var loadedPost = db.Posts.Single(x => x.PostId == firstPostUntracked.PostId);
- Assert.GreaterOrEqual(loadedPost.LastUpdated.Subtract(originalDateTime).Milliseconds, 400);
+ ClassicAssert.GreaterOrEqual(loadedPost.LastUpdated.Subtract(originalDateTime).TotalMilliseconds, 400);
}
}
[Test]
public void Check31CheckCreateDataOk()
{
- using (var db = new SampleWebAppDb())
+ using (var db = TestDbContext.CreateContext(_connection))
{
//SETUP
var snap = new DbSnapShot(db);
var firstPostUntracked = db.Posts.AsNoTracking().First();
+ var firstTwoTags = db.Tags.Take(2).ToList();
//ATTEMPT
+ firstPostUntracked.PostId = 0;
firstPostUntracked.Title = Guid.NewGuid().ToString();
firstPostUntracked.Blogger = db.Blogs.First();
- firstPostUntracked.Tags = db.Tags.Take(2).ToList();
+ firstPostUntracked.Tags = firstTwoTags;
db.Posts.Add(firstPostUntracked);
- var status = db.SaveChangesWithChecking();
+ db.SaveChanges();
//VERIFY
- status.IsValid.ShouldEqual(true, status.Errors);
snap.CheckSnapShot(db,1,2);
var loadedPost = db.Posts.Include( x => x.Blogger).Include( x => x.Tags).Single(x => x.PostId == firstPostUntracked.PostId);
loadedPost.Blogger.BlogId.ShouldEqual(db.Blogs.First().BlogId);
- CollectionAssert.AreEquivalent(db.Tags.Take(2).Select(x => x.TagId), loadedPost.Tags.Select(x => x.TagId));
+ CollectionAssert.AreEquivalent(firstTwoTags.Select(x => x.TagId), loadedPost.Tags.Select(x => x.TagId));
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Tests/UnitTests/Group03ServiceLayer/Test10DiSimple.cs b/Tests/UnitTests/Group03ServiceLayer/Test10DiSimple.cs
index b502ff5..b15d47e 100644
--- a/Tests/UnitTests/Group03ServiceLayer/Test10DiSimple.cs
+++ b/Tests/UnitTests/Group03ServiceLayer/Test10DiSimple.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: Test10DiSimple.cs
@@ -25,136 +25,129 @@
// SOFTWARE.
#endregion
using System;
-using System.Linq;
-using System.Reflection;
-using Autofac;
-using Autofac.Core;
+using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
+using NUnit.Framework.Legacy;
using Tests.DependencyItems;
using Tests.Helpers;
namespace Tests.UnitTests.Group03ServiceLayer
{
- class Test10DiSimple
+ ///
+ /// The application swapped Autofac for the built-in Microsoft.Extensions.DependencyInjection container.
+ /// These tests assert the same lifetime/registration semantics (transient, singleton, scoped, constructor
+ /// parameters, disposal, open generics, non-resolvable private constructors) against that container.
+ ///
+ public class Test10DiSimple
{
[Test]
- public void Test01AutoFacSimple()
+ public void Test01SimpleResolveOk()
{
//SETUP
- var builder = new ContainerBuilder();
- builder.RegisterType().As();
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient();
+ var provider = services.BuildServiceProvider();
//ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- var instance = lifetimeScope.Resolve();
- Assert.NotNull(instance);
+ var instance = scope.ServiceProvider.GetService();
+ ClassicAssert.NotNull(instance);
(instance is SimpleClass).ShouldEqual(true);
}
-
}
[Test]
- public void Test02AutoFacTransient()
+ public void Test02TransientGivesDifferentInstances()
{
- //Setup
- var builder = new ContainerBuilder();
- builder.RegisterType().As();
- var container = builder.Build();
+ //SETUP
+ var services = new ServiceCollection();
+ services.AddTransient();
+ var provider = services.BuildServiceProvider();
- //Attempt
+ //ATTEMPT
ISimpleClass instance1;
- using (var lifetimeScope = container.BeginLifetimeScope())
- instance1 = lifetimeScope.Resolve();
+ using (var scope = provider.CreateScope())
+ instance1 = scope.ServiceProvider.GetService();
ISimpleClass instance2;
- using (var lifetimeScope = container.BeginLifetimeScope())
- instance2 = lifetimeScope.Resolve();
-
- //Verify
- Assert.NotNull(instance1);
- Assert.NotNull(instance2);
- Assert.AreNotSame(instance1, instance2);
+ using (var scope = provider.CreateScope())
+ instance2 = scope.ServiceProvider.GetService();
+ //VERIFY
+ ClassicAssert.NotNull(instance1);
+ ClassicAssert.NotNull(instance2);
+ ClassicAssert.AreNotSame(instance1, instance2);
}
[Test]
- public void Test03AutoFacSingle()
+ public void Test03SingletonGivesSameInstance()
{
- //Setup
- var builder = new ContainerBuilder();
- builder.RegisterType().As().SingleInstance();
- var container = builder.Build();
+ //SETUP
+ var services = new ServiceCollection();
+ services.AddSingleton();
+ var provider = services.BuildServiceProvider();
- //Attempt
+ //ATTEMPT
ISimpleClass instance1;
- using (var lifetimeScope = container.BeginLifetimeScope())
- instance1 = lifetimeScope.Resolve();
+ using (var scope = provider.CreateScope())
+ instance1 = scope.ServiceProvider.GetService();
ISimpleClass instance2;
- using (var lifetimeScope = container.BeginLifetimeScope())
- instance2 = lifetimeScope.Resolve();
-
- //Verify
- Assert.NotNull(instance1);
- Assert.NotNull(instance2);
- Assert.AreSame(instance1, instance2);
+ using (var scope = provider.CreateScope())
+ instance2 = scope.ServiceProvider.GetService();
+ //VERIFY
+ ClassicAssert.NotNull(instance1);
+ ClassicAssert.NotNull(instance2);
+ ClassicAssert.AreSame(instance1, instance2);
}
[Test]
- public void Test04AutoFacLifeTimeScope()
+ public void Test04ScopedGivesSameInstanceWithinScopeButDifferentAcrossScopes()
{
- //Setup
- var builder = new ContainerBuilder();
- builder.RegisterType().As().InstancePerLifetimeScope();
- var container = builder.Build();
+ //SETUP
+ var services = new ServiceCollection();
+ services.AddScoped();
+ var provider = services.BuildServiceProvider();
- //Attempt and VERIFY
+ //ATTEMPT and VERIFY
ISimpleClass scope1Instance1;
- ISimpleClass scope1Instance2;
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- scope1Instance1 = lifetimeScope.Resolve();
- scope1Instance2 = lifetimeScope.Resolve();
- Assert.NotNull(scope1Instance1);
- Assert.NotNull(scope1Instance2);
- Assert.AreSame(scope1Instance1, scope1Instance2);
+ scope1Instance1 = scope.ServiceProvider.GetService();
+ var scope1Instance2 = scope.ServiceProvider.GetService();
+ ClassicAssert.NotNull(scope1Instance1);
+ ClassicAssert.NotNull(scope1Instance2);
+ ClassicAssert.AreSame(scope1Instance1, scope1Instance2);
}
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- ISimpleClass scope2Instance1 = lifetimeScope.Resolve();
- Assert.NotNull(scope2Instance1);
- Assert.NotNull(scope1Instance1);
- Assert.NotNull(scope1Instance2);
- Assert.AreNotSame(scope1Instance1, scope2Instance1);
- Assert.AreNotSame(scope1Instance1, scope2Instance1);
+ var scope2Instance1 = scope.ServiceProvider.GetService();
+ ClassicAssert.NotNull(scope2Instance1);
+ ClassicAssert.AreNotSame(scope1Instance1, scope2Instance1);
}
-
}
//-----------------------------------------------------------
//item with constructor param
[Test]
- public void Test05AutoFacConstructor()
+ public void Test05ConstructorParameterOk()
{
//SETUP
- var builder = new ContainerBuilder();
- builder.RegisterType().As()
- .WithParameter("myInt", 42);
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient(_ => new ConstructorParamClass(42));
+ var provider = services.BuildServiceProvider();
//ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- var instance = lifetimeScope.Resolve();
- Assert.NotNull(instance);
+ var instance = scope.ServiceProvider.GetService();
+ ClassicAssert.NotNull(instance);
instance.MyInt.ShouldEqual(42);
}
-
}
@@ -164,133 +157,124 @@ public void Test05AutoFacConstructor()
private int _numTimeDisposeCalled;
[Test]
- public void Test15AutoFacDisposeCreate()
+ public void Test15DisposeNotCalledWhileScopeAlive()
{
- //Setup
- var builder = new ContainerBuilder();
+ //SETUP
+ _numTimeDisposeCalled = 0;
Action checker = (() => _numTimeDisposeCalled++);
- builder.RegisterType().As().WithParameter("disposeWasCalled", checker);
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient(_ => new MyDisposableClass(checker));
+ var provider = services.BuildServiceProvider();
- //Attempt
- _numTimeDisposeCalled = 0;
- var mydisp = container.Resolve();
+ //ATTEMPT
+ var scope = provider.CreateScope();
+ var mydisp = scope.ServiceProvider.GetService();
- //Verify
- Assert.NotNull(mydisp);
+ //VERIFY
+ ClassicAssert.NotNull(mydisp);
_numTimeDisposeCalled.ShouldEqual(0);
-
}
[Test]
- public void Test16AutoFacDisposeCalled()
+ public void Test16DisposeCalledWhenScopeDisposed()
{
- //Setup
- var builder = new ContainerBuilder();
+ //SETUP
+ _numTimeDisposeCalled = 0;
Action checker = (() => _numTimeDisposeCalled++);
- builder.RegisterType().As().WithParameter("disposeWasCalled", checker);
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient(_ => new MyDisposableClass(checker));
+ var provider = services.BuildServiceProvider();
- //Attempt
- _numTimeDisposeCalled = 0;
- using (var lifetimeScope = container.BeginLifetimeScope())
+ //ATTEMPT
+ using (var scope = provider.CreateScope())
{
- var mydisp = lifetimeScope.Resolve();
- Assert.NotNull(mydisp);
+ var mydisp = scope.ServiceProvider.GetService();
+ ClassicAssert.NotNull(mydisp);
}
- //Verify
+ //VERIFY
_numTimeDisposeCalled.ShouldEqual(1);
-
}
//--------------------------------------------------------------
//register generic
[Test]
- public void Test20AutoFacRegisterGeneric()
+ public void Test20RegisterOpenGenericOk()
{
//SETUP
- var builder = new ContainerBuilder();
- builder.RegisterGeneric(typeof(GenericInterface<>)).As(typeof(IGenericInterface<>));
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient(typeof(IGenericInterface<>), typeof(GenericInterface<>));
+ var provider = services.BuildServiceProvider();
//ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- var instance = lifetimeScope.Resolve>();
- Assert.NotNull(instance);
+ var instance = scope.ServiceProvider.GetService>();
+ ClassicAssert.NotNull(instance);
(instance is GenericInterface).ShouldEqual(true);
instance.GetTypeName().ShouldEqual(typeof(SimpleClass).Name);
}
-
}
[Test]
- public void Test21AutoFacRegisterGenericAfterRegisterAssembly()
+ public void Test21RegisterOpenGenericAlongsideOtherServicesOk()
{
//SETUP
- var builder = new ContainerBuilder();
- builder.RegisterAssemblyTypes(GetType().Assembly).AsImplementedInterfaces();
- builder.RegisterGeneric(typeof(GenericInterface<>)).As(typeof(IGenericInterface<>));
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient();
+ services.AddTransient(_ => new ConstructorParamClass(1));
+ services.AddTransient(typeof(IGenericInterface<>), typeof(GenericInterface<>));
+ var provider = services.BuildServiceProvider();
//ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- var instance = lifetimeScope.Resolve>();
- Assert.NotNull(instance);
+ var instance = scope.ServiceProvider.GetService>();
+ ClassicAssert.NotNull(instance);
(instance is GenericInterface).ShouldEqual(true);
instance.GetTypeName().ShouldEqual(typeof(SimpleClass).Name);
}
-
}
//---------------------------------------------------------
//tests on what happens if ctor is private
[Test]
- public void Test30AutoFacRegisterClassWithPrivateCtorBad()
+ public void Test30ResolveClassWithPrivateCtorBad()
{
//SETUP
- var builder = new ContainerBuilder();
- builder.RegisterType().As();
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient();
+ var provider = services.BuildServiceProvider();
//ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- var ex = Assert.Throws(() => lifetimeScope.Resolve());
- ex.Message.ShouldStartWith("No constructors on type");
+ //The built-in container can only activate a type through a public constructor.
+ var ex = Assert.Throws(() => scope.ServiceProvider.GetService());
+ ex.Message.ShouldContain("ClassWithPrivateCtor");
}
-
}
[Test]
- public void Test31AutoFacTryCtorWithPrivateCtorAsOptionOk()
+ public void Test31ResolveOpenGenericWithPublicCtorOk()
{
+ //The old Autofac version used an OnActivating hook to inject the resolved type into a class that
+ //itself only had a private-ctor dependency. The built-in container has no activation hook, so this
+ //test now just verifies the open-generic type (which has a public constructor) resolves correctly.
//SETUP
- var builder = new ContainerBuilder();
- builder.RegisterType().As();
- builder.RegisterGeneric(typeof (ClassToTestClassWithPrivateCtor<>))
- .As(typeof (IClassToTestClassWithPrivateCtor<>))
- .OnActivating(e =>
- {
- var interfaceToLookup = e.Instance.GetType().GetGenericArguments()[0];
- var resolvedInterface =
- e.Context.ComponentRegistry.RegistrationsFor(new TypedService(interfaceToLookup)).SingleOrDefault();
- ((ISetType)e.Instance).SetType(resolvedInterface.Activator.LimitType);
- });
- var container = builder.Build();
+ var services = new ServiceCollection();
+ services.AddTransient(typeof(IClassToTestClassWithPrivateCtor<>), typeof(ClassToTestClassWithPrivateCtor<>));
+ var provider = services.BuildServiceProvider();
//ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
+ using (var scope = provider.CreateScope())
{
- var instance = lifetimeScope.Resolve>();
- Assert.NotNull(instance);
+ var instance = scope.ServiceProvider.GetService>();
+ ClassicAssert.NotNull(instance);
(instance is ClassToTestClassWithPrivateCtor).ShouldEqual(true);
}
-
}
}
}
diff --git a/Tests/UnitTests/Group03ServiceLayer/Test11AutoFacModules.cs b/Tests/UnitTests/Group03ServiceLayer/Test11AutoFacModules.cs
deleted file mode 100644
index 3d4ff00..0000000
--- a/Tests/UnitTests/Group03ServiceLayer/Test11AutoFacModules.cs
+++ /dev/null
@@ -1,167 +0,0 @@
-#region licence
-// The MIT License (MIT)
-//
-// Filename: Test11AutoFacModules.cs
-// Date Created: 2014/05/22
-//
-// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
-//
-// Permission is hereby granted, free of charge, to any person obtaining a copy
-// of this software and associated documentation files (the "Software"), to deal
-// in the Software without restriction, including without limitation the rights
-// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-// copies of the Software, and to permit persons to whom the Software is
-// furnished to do so, subject to the following conditions:
-//
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-//
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-#endregion
-using System.Linq;
-using Autofac;
-using DataLayer.DataClasses;
-using DataLayer.DataClasses.Concrete;
-using DataLayer.Startup;
-using GenericServices;
-using GenericServices.Services.Concrete;
-using NUnit.Framework;
-using SampleWebApp.Infrastructure;
-using ServiceLayer.Startup;
-using Tests.Helpers;
-
-namespace Tests.UnitTests.Group03ServiceLayer
-{
- [TestFixture]
- public class Test11AutoFacModules
- {
-
- [TestFixtureSetUp]
- public void FixtureSetUp()
- {
- using (var db = new SampleWebAppDb())
- {
- DataLayerInitialise.InitialiseThis(false, true);
- DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
- }
- }
-
-
- //-------------------------------------
- //DataLayer
-
- [Test]
- public void CheckSetupDbContextLifetimeScopeItems()
- {
- //SETUP
- var builder = new ContainerBuilder();
- builder.RegisterModule( new DataLayerModule());
- var container = builder.Build();
-
- //ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
- {
- var instance1 = lifetimeScope.Resolve();
- var instance2 = lifetimeScope.Resolve();
- Assert.NotNull(instance1);
- (instance1 is SampleWebAppDb).ShouldEqual(true);
- Assert.AreSame(instance1, instance2); //check that lifetimescope is working
- }
- }
-
-
- //---------------------------------------------
- //ServiceLayer, which also resolves DataLayer
-
- [Test]
- public void Test10ServiceSetupServiceLayer()
- {
- //SETUP
- var builder = new ContainerBuilder();
- builder.RegisterModule(new ServiceLayerModule());
- var container = builder.Build();
-
- //ATTEMPT & VERIFY
- CheckExampleServicesResolve(container);
- }
-
-
- [Test]
- public void Test15SetupServiceLayerDirectGenerics()
- {
- //SETUP
- var builder = new ContainerBuilder();
- builder.RegisterModule(new ServiceLayerModule());
- var container = builder.Build();
-
- //ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
- {
- var instance = lifetimeScope.Resolve();
- Assert.NotNull(instance);
- (instance is ListService).ShouldEqual(true);
- }
- }
-
- [Test]
- public void Test16UseServiceLayerDirectGenerics()
- {
- //SETUP
- var builder = new ContainerBuilder();
- builder.RegisterModule(new ServiceLayerModule());
- var container = builder.Build();
-
- //ATTEMPT & VERIFY
- using (var lifetimeScope = container.BeginLifetimeScope())
- {
- var service = lifetimeScope.Resolve();
- var posts = service.GetAll().ToList();
- posts.Count.ShouldEqual(3);
- }
- }
-
- //------------------------------------------------------
- //MVC layer
-
- [Test]
- public void Test20ViaMvcSetup()
- {
- //SETUP
- var container = AutofacDi.SetupDependency();
-
- //ATTEMPT & VERIFY
- CheckExampleServicesResolve(container);
-
- }
-
- //-------------------------------------------------------
- //private helper
-
- private static void CheckExampleServicesResolve(IContainer container)
- {
- using (var lifetimeScope = container.BeginLifetimeScope())
- {
- //DataLayer - Data classes
-
- //DataLayer - repositories
- var db1 = lifetimeScope.Resolve();
- var db2 = lifetimeScope.Resolve();
- Assert.NotNull(db1);
- Assert.AreSame(db1, db2); //check that lifetimescope is working
-
- //ServiceLayer - complex
- var service1 = lifetimeScope.Resolve();
- var service2 = lifetimeScope.Resolve();
- Assert.NotNull(service1);
- Assert.AreNotSame(service1, service2); //check transient
- (service1 is ListService).ShouldEqual(true);
- }
- }
- }
-}
diff --git a/Tests/UnitTests/Group03ServiceLayer/Test11DiExtensions.cs b/Tests/UnitTests/Group03ServiceLayer/Test11DiExtensions.cs
new file mode 100644
index 0000000..3ee6f2e
--- /dev/null
+++ b/Tests/UnitTests/Group03ServiceLayer/Test11DiExtensions.cs
@@ -0,0 +1,163 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: Test11AutoFacModules.cs
+// Date Created: 2014/05/22
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System;
+using System.Linq;
+using BizLayer.Startup;
+using DataLayer.DataClasses;
+using DataLayer.Startup;
+using GenericServices;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using NUnit.Framework;
+using NUnit.Framework.Legacy;
+using ServiceLayer.PostServices;
+using ServiceLayer.Startup;
+using Tests.Helpers;
+
+namespace Tests.UnitTests.Group03ServiceLayer
+{
+ ///
+ /// The Autofac modules (DataLayerModule/ServiceLayerModule/BizLayerModule) were replaced by the
+ /// IServiceCollection extension methods AddServiceLayer()/AddBizLayer(). This fixture builds a
+ /// ServiceCollection the same way the web app's Program.cs does and asserts the service layer resolves
+ /// and behaves (ICrudServices/IPostCrudHelper resolve, the DbContext is scoped, and a read works).
+ ///
+ [TestFixture]
+ public class Test11DiExtensions
+ {
+ private SqliteConnection _connection;
+ private ServiceProvider _provider;
+
+ [OneTimeSetUp]
+ public void FixtureSetUp()
+ {
+ _connection = TestDbContext.CreateOpenConnection();
+
+ var services = new ServiceCollection();
+ services.AddDbContext(options => options.UseSqlite(_connection));
+ services.AddServiceLayer();
+ services.AddBizLayer();
+ _provider = services.BuildServiceProvider();
+
+ using (var scope = _provider.CreateScope())
+ {
+ var db = scope.ServiceProvider.GetRequiredService();
+ DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
+ }
+ }
+
+ [OneTimeTearDown]
+ public void FixtureTearDown()
+ {
+ _provider?.Dispose();
+ _connection?.Dispose();
+ }
+
+ //-------------------------------------
+ //DataLayer - the DbContext must be scoped
+
+ [Test]
+ public void CheckDbContextIsScoped()
+ {
+ //SETUP & ATTEMPT & VERIFY
+ SampleWebAppDb scope1Instance;
+ using (var scope = _provider.CreateScope())
+ {
+ scope1Instance = scope.ServiceProvider.GetRequiredService();
+ var sameScopeInstance = scope.ServiceProvider.GetRequiredService();
+ ClassicAssert.NotNull(scope1Instance);
+ ClassicAssert.AreSame(scope1Instance, sameScopeInstance); //same instance within a scope
+ }
+
+ using (var scope = _provider.CreateScope())
+ {
+ var scope2Instance = scope.ServiceProvider.GetRequiredService();
+ ClassicAssert.AreNotSame(scope1Instance, scope2Instance); //different instance across scopes
+ }
+ }
+
+ //---------------------------------------------
+ //ServiceLayer, which also resolves DataLayer
+
+ [Test]
+ public void Test10ServiceLayerServicesResolve()
+ {
+ //SETUP & ATTEMPT & VERIFY
+ using (var scope = _provider.CreateScope())
+ {
+ var crudServices = scope.ServiceProvider.GetService();
+ var crudServicesAsync = scope.ServiceProvider.GetService();
+ var postCrudHelper = scope.ServiceProvider.GetService();
+
+ ClassicAssert.NotNull(crudServices);
+ ClassicAssert.NotNull(crudServicesAsync);
+ ClassicAssert.NotNull(postCrudHelper);
+ }
+ }
+
+ [Test]
+ public void Test16UseCrudServicesReadPosts()
+ {
+ //SETUP & ATTEMPT
+ using (var scope = _provider.CreateScope())
+ {
+ var service = scope.ServiceProvider.GetRequiredService();
+ var posts = service.ReadManyNoTracked().ToList();
+
+ //VERIFY
+ posts.Count.ShouldEqual(3);
+ }
+ }
+
+ ///
+ /// The Post DTOs expose the Tag entity collection directly, so the GenericServices read
+ /// projection has to project Post.Tags into DTO.Tags. This asserts the projection really does
+ /// fill that collection, which is what the computed TagNames property renders.
+ ///
+ [Test]
+ public void Test17CrudServicesReadProjectsTagsCollection()
+ {
+ //SETUP & ATTEMPT
+ using (var scope = _provider.CreateScope())
+ {
+ var service = scope.ServiceProvider.GetRequiredService();
+ var postId = service.ReadManyNoTracked().First().PostId;
+
+ var simple = service.ReadManyNoTracked()
+ .Single(x => x.PostId == postId);
+ var detail = service.ReadSingle(postId);
+
+ //VERIFY
+ ClassicAssert.NotNull(simple.Tags);
+ ClassicAssert.NotNull(detail.Tags);
+ ClassicAssert.IsTrue(detail.Tags.Any());
+ ClassicAssert.IsFalse(string.IsNullOrEmpty(detail.TagNames));
+ }
+ }
+ }
+}
diff --git a/Tests/UnitTests/Group03ServiceLayer/Test12PostDtoValidation.cs b/Tests/UnitTests/Group03ServiceLayer/Test12PostDtoValidation.cs
new file mode 100644
index 0000000..41f4f66
--- /dev/null
+++ b/Tests/UnitTests/Group03ServiceLayer/Test12PostDtoValidation.cs
@@ -0,0 +1,115 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: Test12PostDtoValidation.cs
+// Date Created: 2014/08/16
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System.Collections.Generic;
+using DataLayer.DataClasses.Concrete;
+using NUnit.Framework;
+using ServiceLayer.PostServices;
+using Tests.Helpers;
+
+namespace Tests.UnitTests.Group03ServiceLayer
+{
+ ///
+ /// On a Create/Edit POST the DetailPostDto is model-bound and ASP.NET Core's validation visitor reads
+ /// every gettable property, including the computed TagNames. The Tags collection is not posted, so it
+ /// is null at that point; TagNames must not throw (an unhandled ArgumentNullException surfaced as an
+ /// HTTP 500 and stopped Posts create/edit before the controller ran).
+ ///
+ public class Test12PostDtoValidation
+ {
+ [Test]
+ public void Check01TagNamesWhenTagsNullReturnsEmpty()
+ {
+ //SETUP
+ var dto = new DetailPostDto { Tags = null };
+
+ //ATTEMPT
+ var tagNames = dto.TagNames;
+
+ //VERIFY
+ tagNames.ShouldEqual(string.Empty);
+ }
+
+ [Test]
+ public void Check02TagNamesAsyncWhenTagsNullReturnsEmpty()
+ {
+ //SETUP
+ var dto = new DetailPostDtoAsync { Tags = null };
+
+ //ATTEMPT
+ var tagNames = dto.TagNames;
+
+ //VERIFY
+ tagNames.ShouldEqual(string.Empty);
+ }
+
+ [Test]
+ public void Check03SimpleTagNamesWhenTagsNullReturnsEmpty()
+ {
+ //SETUP
+ var dto = new SimplePostDto { Tags = null };
+
+ //ATTEMPT
+ var tagNames = dto.TagNames;
+
+ //VERIFY
+ tagNames.ShouldEqual(string.Empty);
+ }
+
+ [Test]
+ public void Check04SimpleTagNamesAsyncWhenTagsNullReturnsEmpty()
+ {
+ //SETUP
+ var dto = new SimplePostDtoAsync { Tags = null };
+
+ //ATTEMPT
+ var tagNames = dto.TagNames;
+
+ //VERIFY
+ tagNames.ShouldEqual(string.Empty);
+ }
+
+ [Test]
+ public void Check05TagNamesWhenTagsPopulatedJoinsNames()
+ {
+ //SETUP
+ var dto = new DetailPostDto
+ {
+ Tags = new List
+ {
+ new Tag { Name = "Programming" },
+ new Tag { Name = "Education" }
+ }
+ };
+
+ //ATTEMPT
+ var tagNames = dto.TagNames;
+
+ //VERIFY
+ tagNames.ShouldEqual("Programming, Education");
+ }
+ }
+}
diff --git a/Tests/UnitTests/Group03ServiceLayer/Test13SlugUniqueness.cs b/Tests/UnitTests/Group03ServiceLayer/Test13SlugUniqueness.cs
new file mode 100644
index 0000000..b8d6e58
--- /dev/null
+++ b/Tests/UnitTests/Group03ServiceLayer/Test13SlugUniqueness.cs
@@ -0,0 +1,114 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: Test13SlugUniqueness.cs
+// Date Created: 2014/05/22
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System.Linq;
+using DataLayer.DataClasses;
+using DataLayer.DataClasses.Concrete;
+using DataLayer.Startup;
+using GenericServices;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using NUnit.Framework;
+using ServiceLayer.Startup;
+using Tests.Helpers;
+
+namespace Tests.UnitTests.Group03ServiceLayer
+{
+ ///
+ /// A duplicate Tag.Slug is rejected by the data layer with a ValidationException on a direct SaveChanges,
+ /// but going through GenericServices that would escape as an unhandled HTTP 500. AddServiceLayer wires a
+ /// GenericServices BeforeSaveChanges hook so the same problem is reported as a status/validation error
+ /// (which controllers copy into ModelState) and SaveChanges is never reached.
+ ///
+ public class Test13SlugUniqueness
+ {
+ private SqliteConnection _connection;
+ private ServiceProvider _provider;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _connection = TestDbContext.CreateOpenConnection();
+
+ var services = new ServiceCollection();
+ services.AddDbContext(options => options.UseSqlite(_connection));
+ services.AddServiceLayer();
+ _provider = services.BuildServiceProvider();
+
+ using (var scope = _provider.CreateScope())
+ {
+ var db = scope.ServiceProvider.GetRequiredService();
+ DataLayerInitialise.ResetBlogs(db, TestDataSelection.Small);
+ }
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ _provider?.Dispose();
+ _connection?.Dispose();
+ }
+
+ [Test]
+ public void Check01CreateTagWithUniqueSlugOk()
+ {
+ using (var scope = _provider.CreateScope())
+ {
+ //SETUP
+ var service = scope.ServiceProvider.GetRequiredService();
+ var startCount = service.ReadManyNoTracked().Count();
+
+ //ATTEMPT
+ service.CreateAndSave(new Tag { Name = "brand new", Slug = "brandnew" });
+
+ //VERIFY
+ service.IsValid.ShouldEqual(true);
+ service.ReadManyNoTracked().Count().ShouldEqual(startCount + 1);
+ }
+ }
+
+ [Test]
+ public void Check05CreateTagWithDuplicateSlugReturnsErrorNotException()
+ {
+ using (var scope = _provider.CreateScope())
+ {
+ //SETUP
+ var service = scope.ServiceProvider.GetRequiredService();
+ var existingSlug = service.ReadManyNoTracked().First().Slug;
+ var startCount = service.ReadManyNoTracked().Count();
+
+ //ATTEMPT
+ service.CreateAndSave(new Tag { Name = "duplicate slug", Slug = existingSlug });
+
+ //VERIFY
+ service.IsValid.ShouldEqual(false);
+ service.GetAllErrors().ShouldEqual("The Slug on tag 'duplicate slug' must be unique and is already being used.");
+ service.ReadManyNoTracked().Count().ShouldEqual(startCount);
+ }
+ }
+ }
+}
diff --git a/Tests/UnitTests/Group03ServiceLayer/Test14PostCrudHelperTracking.cs b/Tests/UnitTests/Group03ServiceLayer/Test14PostCrudHelperTracking.cs
new file mode 100644
index 0000000..5a84cac
--- /dev/null
+++ b/Tests/UnitTests/Group03ServiceLayer/Test14PostCrudHelperTracking.cs
@@ -0,0 +1,79 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: Test14PostCrudHelperTracking.cs
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System.Linq;
+using DataLayer.DataClasses.Concrete;
+using Microsoft.Data.Sqlite;
+using Microsoft.EntityFrameworkCore;
+using NUnit.Framework;
+using ServiceLayer.PostServices;
+using Tests.Helpers;
+
+namespace Tests.UnitTests.Group03ServiceLayer
+{
+ ///
+ /// A create that fails validation must not leave an Added Post behind in the request-scoped
+ /// change tracker, otherwise any later SaveChanges on the same scope would persist a
+ /// half-built row that the user never successfully created.
+ ///
+ public class Test14PostCrudHelperTracking
+ {
+ private SqliteConnection _connection;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _connection = TestDbContext.CreateOpenConnection();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ _connection.Dispose();
+ }
+
+ [Test]
+ public void Check01FailedCreateLeavesNoAddedPostTracked()
+ {
+ //SETUP
+ using var db = TestDbContext.CreateContext(_connection);
+ var helper = new PostCrudHelper(db);
+ //no blogger selected and no tags selected, so the create must fail
+ var dto = new DetailPostDto { Title = "Test title", Content = "Test content" };
+
+ //ATTEMPT
+ var status = helper.CreatePost(dto);
+
+ //VERIFY
+ Assert.That(status.IsValid, Is.False);
+ Assert.That(db.ChangeTracker.Entries()
+ .Any(x => x.State == EntityState.Added), Is.False);
+
+ //a later save on the same scope must not persist anything
+ db.SaveChanges();
+ Assert.That(db.Posts.Count(), Is.EqualTo(0));
+ }
+ }
+}
diff --git a/Tests/UnitTests/Group06Mvc/Test02Validation.cs b/Tests/UnitTests/Group06Mvc/Test02Validation.cs
index 7ef1d18..5037ef2 100644
--- a/Tests/UnitTests/Group06Mvc/Test02Validation.cs
+++ b/Tests/UnitTests/Group06Mvc/Test02Validation.cs
@@ -1,4 +1,4 @@
-#region licence
+#region licence
// The MIT License (MIT)
//
// Filename: Test02Validation.cs
@@ -24,17 +24,14 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#endregion
-using System;
using System.Linq;
-using GenericLibsBase.Core;
-using GenericServices.Core;
using NUnit.Framework;
-using SampleWebApp.Infrastructure;
+using NUnit.Framework.Legacy;
using Tests.Helpers;
namespace Tests.UnitTests.Group06Mvc
{
- class Test02Validation
+ public class Test02Validation
{
[Test]
@@ -66,7 +63,7 @@ public void Check05TestModelStateValidateOnly()
//VERIFY
modelState.IsValid.ShouldEqual(false);
- modelState.Keys.Count.ShouldEqual(1);
+ modelState.Keys.Count().ShouldEqual(1);
modelState.Keys.First().ShouldEqual("");
modelState[modelState.Keys.First()].Errors.Count.ShouldEqual(2);
modelState[modelState.Keys.First()].Errors[0].ErrorMessage.ShouldEqual("This is a top level error caused by CreateValidationError being set.");
@@ -84,7 +81,7 @@ public void Check06TestModelStateValidateOneOnly()
//VERIFY
modelState.IsValid.ShouldEqual(false);
- modelState.Keys.Count.ShouldEqual(1);
+ modelState.Keys.Count().ShouldEqual(1);
modelState.Keys.First().ShouldEqual("");
modelState[modelState.Keys.First()].Errors.Count.ShouldEqual(1);
modelState[modelState.Keys.First()].Errors[0].ErrorMessage.ShouldEqual("This is a top level error caused by CreateValidationError being set.");
@@ -101,7 +98,7 @@ public void Check06TestModelStateIntAttributeOnly()
//VERIFY
modelState.IsValid.ShouldEqual(false);
- modelState.Keys.Count.ShouldEqual(1);
+ modelState.Keys.Count().ShouldEqual(1);
modelState.Keys.First().ShouldEqual("MyInt");
modelState[modelState.Keys.First()].Errors.Count.ShouldEqual(1);
modelState[modelState.Keys.First()].Errors[0].ErrorMessage.ShouldEqual("The field MyInt must be between 0 and 100.");
@@ -119,7 +116,7 @@ public void Check07TestModelStateStringAttributeOnly()
//VERIFY
modelState.IsValid.ShouldEqual(false);
- modelState.Keys.Count.ShouldEqual(1);
+ modelState.Keys.Count().ShouldEqual(1);
modelState.Keys.First().ShouldEqual("MyString");
CollectionAssert.AreEquivalent(new[]
{
@@ -148,126 +145,9 @@ public void Check08TestModelStateMixedErrorsOnly()
}
//---------------------------------------------------------------------
- //now use to check ValidationHelper ReturnModelErrorsAsJson
-
- [Test]
- public void Check15TestModelStateValidateOnly()
- {
- //SETUP
- var model = new ModelStateTester.TestModel("123", 50, true);
-
- //ATTEMPT
- var jsonResult = model.ReturnModelState().ReturnModelErrorsAsJson();
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- json.ShouldEqual("{\"errorsDict\":{\"\":{\"errors\":[\"This is a top level error caused by CreateValidationError being set.\",\"This is a top level error caused by MyInt having value 50.\"]}}}");
- }
-
- [Test]
- public void Check16TestModelStateValidateOneOnly()
- {
- //SETUP
- var model = new ModelStateTester.TestModel("123", 2, true);
-
- //ATTEMPT
- var jsonResult = model.ReturnModelState().ReturnModelErrorsAsJson();
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- json.ShouldEqual("{\"errorsDict\":{\"\":{\"errors\":[\"This is a top level error caused by CreateValidationError being set.\"]}}}");
- }
-
-
- [Test]
- public void Check16TestModelStateIntAttributeOnly()
- {
- //SETUP
- var model = new ModelStateTester.TestModel("123", -1, false);
-
- //ATTEMPT
- var jsonResult = model.ReturnModelState().ReturnModelErrorsAsJson();
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- json.ShouldEqual("{\"errorsDict\":{\"MyInt\":{\"errors\":[\"The field MyInt must be between 0 and 100.\"]}}}");
- }
-
- [Test]
- public void Check17TestModelStateStringAttributeOnly()
- {
- //SETUP
- var model = new ModelStateTester.TestModel("", 2, false);
-
- //ATTEMPT
- var jsonResult = model.ReturnModelState().ReturnModelErrorsAsJson();
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- const string order1 = "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The field MyString must be a string or array type with a minimum length of '2'.\",\"The MyString field is required.\"]}}}";
- const string order1Json = "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The field MyString must be a string or array type with a minimum length of \\u00272\\u0027.\",\"The MyString field is required.\"]}}}";
-
- const string order2 = "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The MyString field is required.\",\"The field MyString must be a string or array type with a minimum length of '2'.\"]}}}";
- const string order2Json =
- "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The MyString field is required.\",\"The field MyString must be a string or array type with a minimum length of \\u00272\\u0027.\"]}}}";
- (json == order1Json || json == order2Json).ShouldEqual(true);
-
- }
-
- [Test]
- public void Check18TestModelStateMixedErrorsOnly()
- {
- //SETUP
- var model = new ModelStateTester.TestModel("", -1, true);
-
- //ATTEMPT
- var jsonResult = model.ReturnModelState().ReturnModelErrorsAsJson();
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- const string order1 = "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The field MyString must be a string or array type with a minimum length of '2'.\",\"The MyString field is required.\"]},";
- const string order1Json = "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The field MyString must be a string or array type with a minimum length of \\u00272\\u0027.\",\"The MyString field is required.\"]},";
- const string order2 = "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The MyString field is required.\",\"The field MyString must be a string or array type with a minimum length of '2'.\"]},";
- const string order2Json =
- "{\"errorsDict\":{\"MyString\":{\"errors\":[\"The MyString field is required.\",\"The field MyString must be a string or array type with a minimum length of \\u00272\\u0027.\"]},";
- const string part2 = "\"MyInt\":{\"errors\":[\"The field MyInt must be between 0 and 100.\"]}}}";
- (json == order1Json + part2 || json == order2Json + part2).ShouldEqual(true);
- }
-
- //-------------------------------------------------------------------
- //now the ReturnErrorsAsJson
-
- [Test]
- public void Check20StatusToJsonTopLevel()
- {
- //SETUP
- var status = new SuccessOrErrors();
- var dto = new {MyInt = 1};
-
- //ATTEMPT
- status.AddSingleError("This is a top level error.");
- var jsonResult = status.ReturnErrorsAsJson(dto);
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- json.ShouldEqual("{\"errorsDict\":{\"\":{\"errors\":[\"This is a top level error.\"]}}}");
- }
-
- [Test]
- public void Check21StatusToJsonProperty()
- {
- //SETUP
- var status = new SuccessOrErrors();
- var dto = new { MyInt = 1 };
-
- //ATTEMPT
- status.AddNamedParameterError("MyInt", "This is a property level error.");
- var jsonResult = status.ReturnErrorsAsJson(dto);
-
- //VERIFY
- var json = jsonResult.Data.SerialiseToJson();
- json.ShouldEqual("{\"errorsDict\":{\"MyInt\":{\"errors\":[\"This is a property level error.\"]}}}");
- }
-
+ //The old Check15-Check21 tests exercised SampleWebApp.Infrastructure.ValidationHelper
+ //(ReturnModelErrorsAsJson / JsonNetResult) and the EF6 GenericServices SuccessOrErrors type.
+ //Both of those types were deleted in the ASP.NET Core / EfCore.GenericServices migration, so the
+ //JSON-serialisation tests no longer have anything to test and have been removed.
}
}
diff --git a/Tests/UnitTests/Group06Mvc/Test03PostsControllerEdit.cs b/Tests/UnitTests/Group06Mvc/Test03PostsControllerEdit.cs
new file mode 100644
index 0000000..e30327a
--- /dev/null
+++ b/Tests/UnitTests/Group06Mvc/Test03PostsControllerEdit.cs
@@ -0,0 +1,99 @@
+#region licence
+// The MIT License (MIT)
+//
+// Filename: Test03PostsControllerEdit.cs
+//
+// Copyright (c) 2014 Jon Smith (www.selectiveanalytics.com & www.thereformedprogrammer.net)
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+#endregion
+using System.Threading.Tasks;
+using GenericServices;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+using NUnit.Framework;
+using SampleWebApp.Controllers;
+using ServiceLayer.PostServices;
+
+namespace Tests.UnitTests.Group06Mvc
+{
+ ///
+ /// ICrudServices.ReadSingle returns null when the key does not match a row. The Post Edit GET
+ /// actions then hand that result to IPostCrudHelper to fill the blogger dropdown and tag
+ /// multi-select, which dereferences it, so a stale or invalid id must be turned into a
+ /// NotFound result before the secondary data is set up.
+ ///
+ public class Test03PostsControllerEdit
+ {
+ [Test]
+ public void Check01EditWithUnknownIdReturnsNotFound()
+ {
+ //SETUP
+ var service = new Mock();
+ service.Setup(x => x.ReadSingle(It.IsAny