Base solution for your next web application

Activities of "OutdoorEd"

I will be using the multitenant option and want to sell my SaaS product at different levels based on the number of User Licenses for each Tenant - example 5 Users, 10 Users, etc. A Tenant could have as many User Accounts as they want but only the Number of User Licenses purchased could be active at any one time. So the Tenant could purchase a 10 User License and could create 10 Active Users. In order to add another Active User, they would have to Deactivate one user. So they could have 11 users (or more) but only 10 Active Users. This would be a great addition to my project.

Is this a feature that you would consider adding?

I am using ASP.NET Zero as the authentication platform for a multi-tenant application. Once logged in individual tenants submit accident data to a separate database. I need to do two things with TenantId.

  1. Filter accident data by TenantId so that each Tenant can only view their own data. I have a TenantId as a field in the Accident table. I think this can be done in my AccidentModel using some code for EntityFramework.Dynamic Filters but do not know how to implement the code.

  2. I need to be able to access the TenantID as a Claim or Session variable for CRUD operations and don't know what code I would need to do that.

Thank you

There are dozens of child tables so I've simplified the model to show a short version the main parent table INCIDENT and just one child table - COMMUNICATIONLOG. TenantId is saved in the Incidents Table and also is saved in all the child tables. Having TenantId in all child tables provides row level security.

INCIDENTS namespace OE_Tenant.Web.Areas.Incidents.Models { using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.Spatial;

[Table("idb_Incident")]
public partial class Incident
{
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
    public Incident()
    {
        CommunicationLog = new HashSet<CommunicationLog>();
    }

    [Key]
    public Guid IncidentId { get; set; }

    [UIHint("GridForeignKey")]
    public int IncidentCategoryId { get; set; }

    [Required]
    [Column(TypeName = "smalldatetime")]
    public DateTime? IncidentDate { get; set; }

    [Required]
    [StringLength(200)]
    public string IncidentEvent { get; set; }

    [UIHint("GridForeignKey")]
    public int IncidentTypeId { get; set; }

    [Column(TypeName = "datetime2")]
    public DateTime DateEntered { get; set; }

	[Column(TypeName = "datetime2")]
    public DateTime? DateUpdated { get; set; }

    public int TenantId { get; set; }

    public virtual LkpIncidentCategory LkpIncidentCategory { get; set; }

    public virtual LkpIncidentType LkpIncidentType { get; set; }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
    public virtual ICollection<CommunicationLog> CommunicationLog { get; set; }

}

}

COMMUNICATIONLOG

using System.ComponentModel;

namespace OE_Tenant.Web.Areas.Incidents.Models

{ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.Spatial;

[Table("idb_CommunicationLog")]
public partial class CommunicationLog
{

    [Key]
    public Guid CommunicationLogId { get; set; }

    [DisplayName("Communication Type")]
    [UIHint("GridForeignKey")]
    public int CommunicationTypeId { get; set; }

    [DisplayName("Direction")]
    [UIHint("GridForeignKey")]
    public int CommunicationDirectionId { get; set; }

    public Guid IncidentId { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:MM/dd/yyyy}")]
    public DateTime? Date { get; set; }

    [DataType(DataType.Time)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:t}")]
    public TimeSpan? StartTime { get; set; }

    [DataType(DataType.Time)]
    [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:t}")]
    public TimeSpan? EndTime { get; set; }

    [StringLength(50)]
    public string FirstName { get; set; }

    [StringLength(50)]
    [Display(Name = "Last Name", Prompt = "Enter person's last name")]
    public string LastName { get; set; }

    [StringLength(200)]
    public string Address1 { get; set; }

    [StringLength(100)]
    public string CountryCode { get; set; }

    [Column(TypeName = "datetime2")]
    public DateTime DateEntered { get; set; }

    [Column(TypeName = "datetime2")]
    public DateTime DateUpdated { get; set; }

    public int TenantId { get; set; }

    public virtual LkpCommunicationDirection LkpCommunicationDirection { get; set; }

    public virtual LkpCommunicationType LkpCommunicationType { get; set; }

    public virtual Incident Incident { get; set; }

    public virtual LkpCountry LkpCountry { get; set; }
}

}

Do you have a timeline for adding the Subscription Management feature you listed in the roadmap? I need to manage the number of Active Users in a Subscription and am developing an application to be released in May. I have built my own Active User/Subscription and Length of Subscription implementation but might pull that out depending on when this feature will be added and if it will cover these two scenarios:

StartDate & EndDate of Subscription Limit Number of Active Users: a 5 User Subscription allows you to create 5 Active Users and any number of Inactive Users. You can turn off an active user and the turn on an inactive user

Thanks. Here are the features I am looking for with Subscription Management for Tenants

  1. Integrate with Editions Feature so the Subscription is tied to an Edition.
  2. Create a SubscriptionStartDate and SubscriptionEndDate for the Subscription.
  3. All Tenant Logins are filtered by the SubscriptionStartDate and SubscriptionEndDate. If the login attempt is outside of those dates, the user is redirected to a SubscriptionExpired page.
  4. Each Subscription includes an ActiveUserCount. The Tenant Admin can create as many users as they want. When they create a user that user is set as either Active or Inactive. Only Active Users are permitted to Login. The number of Active Users permitted is managed by the ActiveUserCount. When the Admin has reached that limit, they can no longer make a user active. If they change an Active User to Inactive, they free up a space to make a different user active.

For example, the Tenant purchased a 5 User License so the ActiveUserCount = 5. The Admin can add 10 users and set 5 as active. Only those 5 can login. In order to allow the 6th user to login, the Admin must make one of the 5 currently active users inactive and then the 6th user can be set as active. This allows for flexibility in the license. Also it allows for more streamlined user provisioning since the users can all be added at the same time and then easily turned on or off through an editable grid interface.

  1. A Notification System would be connected to the SubscriptionEndDate to send an email reminder to the Tenant Admin when the Subscription was going to expire (ex. 30 days out from the SubscriptionEndDate)

  2. The SuperAdmin Management Interface would include an editable grid showing all Tenants with SubscriptionStartDate, SubscriptionEndDate, ActiveUserCount and CountofUsersCurrentlyActive

For 2, 3 and 4 if Subscription was set as implemented, the Create Tenant Form would include SubscriptionStartDate, SubscriptionEndDate and ActiveUserCount. These would typically <ins>only</ins> be used by the SuperAdmin in the initial provisioning of a new Tenant and would not be accessible to the individual Tenant Admins.

Let me know if this adequately explains how I would need Subscription Management implemented.

I have an existing MVC 5 application that has been using standard ASP.NET Identity for Authentication/Role Authorization. I am porting it over to Abp.

I already have a populated database in my current app. I used EF Database First to generate the Models from the database and then EF MVC Scaffolding to build all of the Controllers and Views for basic CRUD operations. Since that code all works I want to see if I can reuse it. My thought is to take my current Models, Controllers, Views and place then in a new /Area in .Web. My database tables already have all the necessary Abp fields: TenantId, IsDeleted, DeletedUserId, LastModificationTime, LastModifiedUserId, CreationTime, CreatorUserId so no migrations are necessary.

From looking at the sample implementation implementing CRUD on one database table requires about 30 different code files in the various layers spread across .Application, .Core, .EntityFramework and .Web. With over 90 tables in the existing database I am trying to explore alternatives to throwing out all my existing code and rebuilding the entire application from scratch.

My questions: I need to authenticate via Abp Login and pass the TenantId over to my existing Controllers to filter all the records by TenantId and to be able to use the TenantId and UserId in Create and Update methods. Is it possible to inherit the necessary elements from Abp to use my existing Models and scaffolded Controllers without needing to recreate all of the layers in .Application, .Core, .EntityFramework?

If it is possible, what would I need to do to get my existing code to work with TenantId? Things like a second DbContext that inherits from AbpZeroDbContext, and Models that inherit IMustHaveTenant? Or are there any shortcuts to accessing the TenantId that would allow me to use my current code?

Thanks for any advice you can give me.

Is there any way to use EF Scaffolding to generate CRUD operations in controllers that include TenantId for CRUD and filtering <ins>directly</ins>from the Entity without using Dtos, etc.

Thanks. I tried setting this up with both the Default DbContent and separate DBContent but have not been able to get it to work.

My Entity class inherits IMustHaveTenant

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
using Abp.Domain.Entities;

[Table("idb_IncidentDamage")]
public partial class IncidentDamage : IMustHaveTenant
{
    public Guid Id { get; set; }

My Second DbContext inherits AbpDbContext

using System.Data.Entity; using OE_Tenant.Incidents.Entity; using Abp.EntityFramework;

namespace OE_Tenant.EntityFramework { public class IDBDbContext: AbpDbContext { public IDBDbContext() : base("SecondDefault")

I used the EF6 Scaffolding Wizard to generate the Controllers and Views for CRUD operations straight from the Entities and added the necessary references to Abp and inherited from the ControllerBase

When looking at breakpoints in the Controller for the AbpSession.TenantId the correct TenantId <ins>is</ins> in the session variable however, there is no database filtering taking place. All records are shown from all Tenants. There must be something that is missing/needs to be added to the Scaffolded Controller to include the TenantId in filtering and CRUD operations.

Controller using System; using System.Data.Entity; using System.Linq; using System.Net; using System.Web.Mvc; using Abp.Authorization; using OE_Tenant.EntityFramework; using OE_Tenant.Incidents.Entity; using OE_Tenant.Web.Controllers;

namespace OE_Tenant.Web.Areas.Incidents.Controllers { [AbpAuthorize] public class IncidentDamageController : OE_TenantControllerBase //: Controller { private IDBDbContext db = new IDBDbContext();

    // GET: Incidents/IncidentDamage
    public ActionResult Index()
    {
        var tenantId = AbpSession.TenantId;
        var IncidentDamage = db.IncidentDamage.Include(i => i.Incident).Include(i => i.LkpDamageSeverity).Include(i => i.LkpDamageType);
        return View(IncidentDamage.ToList());
    }

This is the full controller generated by EF

using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Linq; using System.Threading.Tasks; using System.Net; using System.Web; using System.Web.Mvc; using Abp.Authorization; using OE_Tenant.EntityFramework; using OE_Tenant.Incidents.Entity; using OE_Tenant.Web.Controllers;

namespace OE_Tenant.Web.Areas.Incidents.Controllers { [AbpAuthorize] public class IncidentDamageController : OE_TenantControllerBase //: Controller { private IDBDbContext db = new IDBDbContext();

    // GET: Incidents/IncidentDamage
    public ActionResult Index()
    {
        var tenantId = AbpSession.TenantId;
        var IncidentDamage = db.IncidentDamage.Include(i => i.Incident).Include(i => i.LkpDamageSeverity).Include(i => i.LkpDamageType);
        return View(IncidentDamage.ToList());
    }

    // GET: Incidents/IncidentDamage/Details/5
    public ActionResult Details(Guid? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        IncidentDamage IncidentDamage = db.IncidentDamage.Find(id);
        if (IncidentDamage == null)
        {
            return HttpNotFound();
        }
        return View(IncidentDamage);
    }

    // GET: Incidents/IncidentDamage/Create
    public ActionResult Create()
    {
        ViewBag.IncidentId = new SelectList(db.Incident, "Id", "IncidentEvent");
        ViewBag.DamageSeverityId = new SelectList(db.LkpDamageSeverity, "Id", "DamageSeverity");
        ViewBag.DamageTypeId = new SelectList(db.LkpDamageType, "Id", "DamageType");
        return View();
    }

    // POST: Incidents/IncidentDamage/Create
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see &lt;a class=&quot;postlink&quot; href=&quot;http://go.microsoft.com/fwlink/?LinkId=317598&quot;&gt;http://go.microsoft.com/fwlink/?LinkId=317598&lt;/a&gt;.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create([Bind(Include = "Id,IncidentId,DamageTypeId,DamageDescription,DamageSeverityId,DamageAmount,IndexIdentity,TenantId,IsDeleted,DeletedUserId,DeletionTime,LastModificationTime,LastModifiedUserId,CreationTime,CreatorUserId")] IncidentDamage IncidentDamage)
    {
        if (ModelState.IsValid)
        {
            IncidentDamage.Id = Guid.NewGuid();
            db.IncidentDamage.Add(IncidentDamage);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        ViewBag.IncidentId = new SelectList(db.Incident, "Id", "Activity", IncidentDamage.IncidentId);
        ViewBag.DamageSeverityId = new SelectList(db.LkpDamageSeverity, "Id", "DamageSeverity", IncidentDamage.DamageSeverityId);
        ViewBag.DamageTypeId = new SelectList(db.LkpDamageType, "Id", "DamageType", IncidentDamage.DamageTypeId);
        return View(IncidentDamage);
    }

    // GET: Incidents/IncidentDamage/Edit/5
    public ActionResult Edit(Guid? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        IncidentDamage IncidentDamage = db.IncidentDamage.Find(id);
        if (IncidentDamage == null)
        {
            return HttpNotFound();
        }
        ViewBag.IncidentId = new SelectList(db.Incident, "Id", "Activity", IncidentDamage.IncidentId);
        ViewBag.DamageSeverityId = new SelectList(db.LkpDamageSeverity, "Id", "DamageSeverity", IncidentDamage.DamageSeverityId);
        ViewBag.DamageTypeId = new SelectList(db.LkpDamageType, "Id", "DamageType", IncidentDamage.DamageTypeId);
        return View(IncidentDamage);
    }

    // POST: Incidents/IncidentDamage/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see &lt;a class=&quot;postlink&quot; href=&quot;http://go.microsoft.com/fwlink/?LinkId=317598&quot;&gt;http://go.microsoft.com/fwlink/?LinkId=317598&lt;/a&gt;.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind(Include = "Id,IncidentId,DamageTypeId,DamageDescription,DamageSeverityId,DamageAmount,IndexIdentity,TenantId,IsDeleted,DeletedUserId,DeletionTime,LastModificationTime,LastModifiedUserId,CreationTime,CreatorUserId")] IncidentDamage IncidentDamage)
    {
        if (ModelState.IsValid)
        {
            db.Entry(IncidentDamage).State = EntityState.Modified;
            db.SaveChanges();
            return RedirectToAction("Index");
        }
        ViewBag.IncidentId = new SelectList(db.Incident, "Id", "Activity", IncidentDamage.IncidentId);
        ViewBag.DamageSeverityId = new SelectList(db.LkpDamageSeverity, "Id", "DamageSeverity", IncidentDamage.DamageSeverityId);
        ViewBag.DamageTypeId = new SelectList(db.LkpDamageType, "Id", "DamageType", IncidentDamage.DamageTypeId);
        return View(IncidentDamage);
    }

    // GET: Incidents/IncidentDamage/Delete/5
    public ActionResult Delete(Guid? id)
    {
        if (id == null)
        {
            return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
        }
        IncidentDamage IncidentDamage = db.IncidentDamage.Find(id);
        if (IncidentDamage == null)
        {
            return HttpNotFound();
        }
        return View(IncidentDamage);
    }

    // POST: Incidents/IncidentDamage/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(Guid id)
    {
        IncidentDamage IncidentDamage = db.IncidentDamage.Find(id);
        db.IncidentDamage.Remove(IncidentDamage);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            db.Dispose();
        }
        base.Dispose(disposing);
    }
}

}

Question

I have added lots of Permission-based pages in my app and having the Permissions section come up as a model requires a lot of scrolling. How can I "turn off" modals for a particular view and just have it render on a regular page. In my case I am looking at the _PermissionsModal.cshtml

I tried to use {DisableValidation] both in individual Actions in the Controller and on the entire Controller but I still get Abp.Runtime.Validation.AbpValidationException: Method arguments are not valid

If I add
Configuration.Modules.AbpMvc().IsValidationEnabledForControllers = false; the MVC validation works however, it means that I have disabled validation for all of the Abp Controllers as well which I don't want to do (things like creating Users/Roles/Tenants etc.)

Showing 1 to 10 of 73 entries