I would, respectfully, disagree with the assertion that ANZ is not a professional level product. What they sell is source code plain and simple and they are up front about what they are selling. Having built a lot of software over the past 20 years and paid other developers a lot of money to write code for me, there is no way that I could get the breadth of what the ANZ folks have built by paying an outside developer just $2500. From scratch would be in the Tens of Thousands of dollars US range. I am happy for what I have paid for since it was much less expensive than anything else I could have paid a developer to code. The quality of the code is also very good. I've had multiple top developers work with it and they have been impressed with the code quality.
I too wish that upgrading was something that was an option because when they release a improvement, I am left in the dust which is disappointing. But that does not mean that ANZ is not a professional grade product. In moving from the MVC Jquery version to the Net Core Jquery version I spent a lot of time documenting where my code fit in with theirs to make an upgrade process more feasible. I believe that there are some things that they could do with their code that would make it easier to upgrade:
There are several things to understand about ASPNETZERO. It is built as a framework to create your Web app on top off and as an upgradable product. They've done a ton of the heavy lifting for you with authentication, authorization, etc. They don't say that there is an upgrade path (although there are some ways to branch that you can find in this forum).
In the case of 6.9 to 7.0 it is a very big UI change since they updated the underlying Metronic theme system (www.keenthemes.com) from the 5.x version to 6.0. That means you would need to change lots in the layout.cshtml files across the entire application.
Very helpful post. One thing I would add for Telerik ASP.NET Core Jquery with Netzero Core 2.2
I found that the Telerik js files need to be in the header rather than at the bottom of the page to work so on the NetZero /Area/Views/Layouts/__Layout.cshtml
I moved the script code up into the header AND there is a Netzero RenderScript flag in the footer which is set to false by default, change this to true when moving the Script Code to the Header
@RenderSection("Scripts", true)
Here is the Code I moved to the header
`
<!-- Dynamic scripts of ABP system (They are created on runtime and can not be bundled) -->
<script src="@(ApplicationPath)AbpServiceProxies/GetAll?v=@(AppTimes.StartupTime.Ticks)" type="text/javascript"></script>
<script src="@(ApplicationPath)AbpScripts/GetScripts?v=@(AppTimes.StartupTime.Ticks)" type="text/javascript"></script>
<script type="text/javascript">
abp.localization.currentCulture = $.extend({}, abp.localization.currentCulture, { displayNameEnglish: '@CultureInfo.CurrentUICulture.EnglishName' });
moment.locale('@(CultureHelper.UsingLunarCalendar ? "en": CultureInfo.CurrentUICulture.Name )'); //Localizing moment.js
</script>
<script src="@(ApplicationPath)view-resources/Areas/Admin/Views/_Bundles/signalr.bundle.min.js" asp-append-version="true"></script>
<script abp-src="/view-resources/Areas/Admin/Views/_Bundles/common-scripts.js" asp-append-version="true"></script>
<script abp-src="/view-resources/Areas/Admin/Views/_Bundles/app-common-scripts.js" asp-append-version="true"></script>
<script abp-src="/view-resources/Areas/Admin/Views/Layout/_Header.js" asp-append-version="true"></script>
<script src="@(ApplicationPath)view-resources/Areas/Admin/Views/Layout/_ThemeSelectionPanel.js" asp-append-version="true"></script>
@if (isChatEnabled)
{
<script src="@(ApplicationPath)view-resources/Areas/Admin/Views/Layout/_ChatBar.js" asp-append-version="true"></script>
<script src="@(ApplicationPath)Common/Scripts/Chat/chat.signalr.js" asp-append-version="true"></script>
}
<script src="[email protected]_Validation_Localization" asp-append-version="true"></script>
<script src="[email protected]_Select_Localization" asp-append-version="true"></script>
<script src="[email protected]_Timeago_Localization" asp-append-version="true"></script>
<script src="[email protected]_Localization" asp-append-version="true"></script>
<!--end::Base Scripts -->
<!--begin::Telerik Scripts -->
<!-- NOTE Jquery already loaded as part of NetZero Bundles above-->
** ** `
I appreciate all the fantastic work that the ASP.NET Zero Team has put into this framework. CSS Theme development is it's own specialty which requires experience in graphic design and UI. I've built dozens of Web apps and while I'm a good programmer, I'm not a graphic designer and I rely on commercial Theme packages to deliver a really good looking final package which I could never build myself. With a framework this complex and robust, I look to the ASP.NET Zero Team to deliver excellent NET Code and understand the need to utilize CSS Themes.
I had this same issue while developing locally. I spent 8 hours on Saturday coding, building and testing and 6 hours on Sunday - absolutely no problems. Then after one build it threw this same error. I had never touched any code related to the friend service. I ended up rebooting my computer (which showed a memory error on shutdown). When I restarted and built my solution again, the problem was gone. Three more hours of building and testing - no problem. Memory leak?
It is working now. Thanks.
Thanks for the reply. In addition to the aspx Page_Load code
protected void Page_Load(object sender, EventArgs e)
{
if (!IocManager.Instance.IocContainer.Resolve<IPermissionChecker>().IsGranted("Pages.Routestore.Routes.Routelist"))
{
Response.Redirect("/Views/Error/NotAuthorized.html");
}
//Show report otherwise
}
}
I added these in as well and it now "respects" Role Permissions.
<ins>AppPermissions.cs</ins>
public const string Pages_Routestore_Routes_Routelist = "Pages.Routestore.Routes.Routelist";
<ins>AppAuthorizationProvider.cs</ins>
pages.CreateChildPermission(AppPermissions.Pages_Routestore_Routes_Routelist, L("Routelist"));
<ins>Tenant.xml</ins>
<text name="Routelist" value="Route List" />
I do see the Permissions Checkbox for this role and when I select it I see that reflected in the database Abp_AppPermissions table
5 Pages.Routestore.Routes.Routelist True 2018-08-13 04:17:59.800 2 NULL 3 UserPermissionSetting 1
This approach works for me to prevent anonymous users from logging in. I need to be able to control access via ABP Roles and can't see how to control that from the PageLoad on the ASPX page.
public partial class RouteList : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IocManager.Instance.IocContainer.Resolve<IPermissionChecker>().IsGranted("Pages.Routestore.Routes.Routelist"))
{
Response.Redirect("/Views/Error/NotAuthorized.html");
}
//Show report otherwise
}
}
In working with an existing database for the MVC Jquery version I used Simon Hughes' EntityFramework Reverse POCO Generator in a separate project just to create the POCOs. One of the nice things in Simon's template is that you can control things like the namespace so even though it was in a separate project, I could generate the correct namespace for my project.
I worked with a wonderful outside developer who laid out the architecture for this (just to admit that I am not smart enough to come up with this myself). Here is how one class was created for my Incident database application. It uses Repositories and separate Configuration files.
I am only showing one database field from the table all the way through and don't show you any of the relationships to other tables to shorten the code. I used generic MVC scaffolding to create the skeleton of the Controller and all the CRUD Views and then replaced things in the Controller with the Repositories.
FOLDER: OE_Tenant.Core\Incidents\Entity\IncidentFile.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
FOLDER: OE_Tenant.Core\Incident\Repos\IIncidentFileRepository.cs
using Abp.Domain.Repositories; using OE_Tenant.Incidents.Entity; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;
FOLDER: OE_Tenant.EntityFramework\EntityConfigurations\IncidentFileConfiguration.cs
using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.ModelConfiguration; using OE_Tenant.Incidents.Entity;
namespace OE_Tenant.EntityConfigurations { public class IncidentFileConfiguration : EntityTypeConfiguration<IncidentFile> { public IncidentFileConfiguration() { // Primary Key this.HasKey(t => t.Id);
// Table & Column Configuration
this.ToTable("idb_IncidentFile");
this.Property(t => t.Id).HasColumnName("Id");
this.Property(t => t.FileTypeId).HasColumnName("FileTypeId");
this.Property(t => t.TenantId).HasColumnName("TenantId");
}
}
}
FOLDER: OE_Tenant.EntityFramework\Repos\IncidentFileRepository.cs
using OE_Tenant.EntityFramework.Repositories; using OE_Tenant.Incidents.Entity; using OE_Tenant.Incidents.Repos; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Abp.EntityFramework; using OE_Tenant.EntityFramework;
namespace OE_Tenant.Repos { public class IncidentFileRepository : OE_TenantRepositoryBase<IncidentFile, Guid>, IIncidentFileRepository { public IncidentFileRepository(IDbContextProvider<OE_TenantDbContext> dbContextProvider) : base(dbContextProvider) { } } }
FOLDER: OE_Tenant.EntityFramework\OE_TenantDbContext.cs
using System.Data.Common; using System.Data.Entity; using Abp.Zero.EntityFramework; using OE_Tenant.Authorization.Roles; using OE_Tenant.Authorization.Users; using OE_Tenant.Chat; using OE_Tenant.Friendships; using OE_Tenant.MultiTenancy; using OE_Tenant.Storage; using System.Data.Entity.Core.Objects; using System.Data.Entity.Infrastructure; using EntityFramework.Functions; using OE_Tenant.Incidents.Entity; //Added for External Configuration Files using OE_Tenant.EntityConfigurations; //Added for Dynamic Filter using EntityFramework.DynamicFilters; using Abp.Domain.Entities;
namespace OE_Tenant.EntityFramework { public class OE_TenantDbContext : AbpZeroDbContext<Tenant, Role, User> { public virtual DbSet<IncidentFile> IncidentFile { get; set; }
public OE_TenantDbContext() : base("Default") { //Disable initializer to disable Migrations Database.SetInitializer<OE_TenantDbContext>(null); this.Configuration.LazyLoadingEnabled = false; this.Configuration.ProxyCreationEnabled = false; }
public OE_TenantDbContext(string nameOrConnectionString)
: base(nameOrConnectionString)
{
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
}
public OE_TenantDbContext(DbConnection existingConnection)
: base(existingConnection, false)
{
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
}
public OE_TenantDbContext(DbConnection existingConnection, bool contextOwnsConnection)
: base(existingConnection, contextOwnsConnection)
{
this.Configuration.LazyLoadingEnabled = false;
this.Configuration.ProxyCreationEnabled = false;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//Added - NEEDS TO BE IN TO PREVENT MUSTHAVETENANT ERROR
base.OnModelCreating(modelBuilder);
modelBuilder.Configurations.Add(new IncidentFileConfiguration());
}
}
}
FOLDER: OE_Tenant.Web\Areas\Incidents\Models\Dto\IncidentFileDto.cs
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Web.Mvc; using Abp.Application.Services.Dto; using Abp.AutoMapper; using OE_Tenant.Incidents.Entity;
namespace OE_Tenant.Web.Areas.Incidents.Models.Dto { [AutoMapTo(typeof(IncidentFile))] [AutoMapFrom(typeof(IncidentFile))] public class IncidentFileDto : FullAuditedEntityDto<Guid> { [Display(Name = "File Type", Prompt = "Select the File Type")] [Required(ErrorMessage = "Please select a File Type")] public int FileTypeId { get; set; } } }
FOLDER: OE_Tenant.Web\Areas\Incidents\ViewModels\IncidentFileViewModel.cs
using System; using Abp.AutoMapper; using OE_Tenant.Incidents.Entity; using OE_Tenant.Web.Areas.Configuration.ViewModels;
namespace OE_Tenant.Web.Areas.Incidents.ViewModels { [AutoMapFrom(typeof(IncidentFile))] [AutoMapTo(typeof(IncidentFile))] public class IncidentFileViewModel { public Guid Id { get; set; } public int FileTypeId { get; set; } public int TenantId { get; set; } } }
FOLDER: OE_Tenant.Web\Areas\Incidents\Controllers\IncidentFileController.cs
using System; using System.Collections; using System.Data.Entity; using System.Threading.Tasks; using System.Net; using System.Web.Mvc; using OE_Tenant.Incidents.Entity; using OE_Tenant.Web.Controllers; using Abp.Runtime.Validation; using OE_Tenant.Incidents.Repos; using Abp.Web.Mvc.Authorization; using OE_Tenant.Authorization; using OE_Tenant.Web.Extensions; using OE_Tenant.Web.Areas.Incidents.ViewModels; using OE_Tenant.Web.Areas.Incidents.Models.Dto; using Abp.AutoMapper; using OE_Tenant.Extensions; using System.Linq;
namespace OE_Tenant.Web.Areas.Incidents.Controllers { [AbpMvcAuthorize(AppPermissions.Pages_Files)] public class FilesController : OE_TenantControllerBase { private readonly IIncidentFileRepository _repoIncidentFile; private readonly ILkpFileTypeRepository _repoLkpFileType;
public FilesController(
IIncidentFileRepository repoIncidentFile,
ILkpFileTypeRepository repoLkpFileType
)
{
_repoIncidentFile = repoIncidentFile;
_repoLkpFileType = repoLkpFileType;
}
[AbpMvcAuthorize(AppPermissions.Pages_Files_Index)]
// GET: Incidents/IncidentFiles
public ActionResult Index()
{
return View();
}
[AbpMvcAuthorize(AppPermissions.Pages_Files_Details)]
// GET: Incidents/IncidentFiles/Details/5
public async Task<ActionResult> Details(Guid? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
IncidentFile incidentFile = await _repoIncidentFile.GetAllIncluding(i => i.LkpFileType).SingleOrDefaultAsync(i => i.Id == id.Value);
if (incidentFile == null)
{
return HttpNotFound();
}
var model = incidentFile.MapTo<IncidentFileDto>();
return View(model);
}
[AbpMvcAuthorize(AppPermissions.Pages_Files_Create)]
// GET: Incidents/IncidentFiles/Create
public async Task<ActionResult> Create()
{
ViewBag.FileTypeId = new SelectList(await _repoLkpFileType.GetAllListAsync(), "Id", "FileType");
return View();
}
// POST: Incidents/IncidentFiles/Create
[DisableValidation]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(IncidentFileDto model)
{
if (ModelState.IsValid)
{
var incidentFileEntity = model.MapTo<IncidentFile>();
await _repoIncidentFile.InsertAsync(incidentFileEntity);
return RedirectToAction("Index");
}
ViewBag.FileTypeId = new SelectList(await _repoLkpFileType.GetAllListAsync(), "Id", "FileType");
return View(model);
}
[AbpMvcAuthorize(AppPermissions.Pages_Files_Edit)]
// GET: Incidents/IncidentFiles/Edit/5
public async Task<ActionResult> Edit(Guid? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
IncidentFile incidentFile = await _repoIncidentFile.GetAsync(id.Value);
if (incidentFile == null)
{
return HttpNotFound();
}
ViewBag.FileTypeId = new SelectList(await _repoLkpFileType.GetAllListAsync(), "Id", "FileType", incidentFile.FileTypeId);
var model = incidentFile.MapTo<IncidentFileDto>();
return View(model);
}
// POST: Incidents/IncidentFiles/Edit/5
[DisableValidation]
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(IncidentFileDto model)
{
if (ModelState.IsValid)
{
IncidentFile incidentFile = await _repoIncidentFile.GetAsync(model.Id);
incidentFile.FileTypeId = model.FileTypeId;
await _repoIncidentFile.UpdateAsync(incidentFile);
return RedirectToAction("Index");
}
ViewBag.FileTypeId = new SelectList(await _repoLkpFileType.GetAllListAsync(), "Id", "FileType", model.FileTypeId);
return View(model);
}
[AbpMvcAuthorize(AppPermissions.Pages_Files_Delete)]
// GET: Incidents/IncidentFiles/Delete/5
public async Task<ActionResult> Delete(Guid? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
IncidentFile incidentFile = await _repoIncidentFile.GetAllIncluding(i => i.LkpFileType).SingleOrDefaultAsync(i => i.Id == id.Value);
if (incidentFile == null)
{
return HttpNotFound();
}
var model = incidentFile.MapTo<IncidentFileDto>();
return View(model);
}
// POST: Incidents/IncidentFiles/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> DeleteConfirmed(Guid id)
{
await _repoIncidentFile.DeleteAsync(id);
return RedirectToAction("Index");
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
//db.Dispose();
}
base.Dispose(disposing);
}
}
}
Metronic is the underlying ASPNETZERO theme using CSS & JS. It's built by a separate company, <a class="postlink" href="http://www.keenthemes.com">www.keenthemes.com</a> and licensed to ASPNETZERO. Because the Metronic theme features are huge (as you can see if you look at their demos) there are lots of things that aren't necessarily used in ASPNETZERO.
Some of the modal dialogs and edit boxes in the backend are coming from Metronic. It would be really helpful if ASPNETZERO would give us a whitelist of the specific JS libraries that are required and then users could remove unnecessary components. It's probably too hard to identify the CSS to be removed.