AutoPopulate Global Request Filter

How do I add a global request filter to autopopulate all requests that has a CompanyId attribute (which is saved in my AppUser table) to my AutoQuery GenerateCrudServices.

Adding that manually to each Crud Request I am doing this:

[AutoPopulate(nameof(AccountType.CompanyId), Eval = “${userSession.CompanyId}”)]

I however want to put that in a global request filter so I don’t have to create manual AutoQuery Requests but can use the GenerateCrudServices and in that add the Autopopulate.

If you just want to populate a Request DTO property you can populate the Request DTO directly:

GlobalRequestFiltersAsync.Add(async (req, res, dto) => {
    if (dto is AccountType createRequest)
    {
        var session = await req.SessionAsAsync<AuthUserSession>();
        createRequest.CompanyId = session.CompanyId;
    }
});

But this requires the property to exist on the Request DTO, if it only exists on the model you would need to add a AutoCrudMetadataFilters with the same syntax as the attribute, e.g:

Plugins.Add(new AutoQueryFeature {
    AutoCrudMetadataFilters = {
      meta => meta.Add(new AutoPopulateAttribute(nameof(AccountType.CompanyId)) {
          Eval = "userSession.CompanyId"
      })
    }
});

I am using GenerateCrudServices, so don’t have the AccountType models or the Request DTO. I need to dynamically loop through them and add the AutoPopulateAttribute to each Model / RequestDTo that has a CompanyId

I was looking at the servicefilter on GenerateCrudServices. The codes below does not work but explains what I would like to do.

GenerateCrudServices = new GenerateCrudServices {
    AutoRegister = true,
    //AddDataContractAttributes = false,
    ServiceFilter = (op,req) =>
    {
        op.Request.AddAttribute(new AutoPopulateAttribute(nameof(req.GetDto()).CompanyId, { Eval = "userSession.CompanyId" });
    }
},

You can’t use nameof on methods, just try using “CompanyId”.

You’ll need to inspect op.Request or its op.Request.Properties to find which Request DTOs you want to apply them to.

All my Request DTO’s have CompanyId field, so I added

GenerateCrudServices = new GenerateCrudServices {
                        AutoRegister = true,
                        //AddDataContractAttributes = false,
                        ServiceFilter = (op, req) =>
                        {
                            var props = op.Request.Properties;
                            op.Request.AddAttribute(new AutoPopulateAttribute("CompanyId") { Eval = "userSession.CompanyId" });
                        }
                    },

But it is not working

If your Request DTO has the CompanyId field than you should use the Request Filter instead.

But if it should always be populated from the Session then it should be removed from the Request DTO:

op.Request.AddAttribute(new AutoPopulateAttribute(nameof(MySession.CompanyId)) {
    Eval = "userSession.CompanyId"
});
op.Request.Properties.RemoveAll(x => x.Name == nameof(MySession.CompanyId));

But if you want to keep it on the Request DTO to update it via the UI you’ll need to set it with the Request Filter. If you don’t have a type to cast it to, then you can dynamically set it using Reflection Utils, e.g:

GlobalRequestFiltersAsync.Add(async (req, res, dto) => {
    if (!HttpUtils.HasRequestBody(req.Verb)) return;
    
    var props = dto.ToObjectDictionary();
    if (!props.ContainsKey(nameof(MySession.CompanyId))) return;
    
    var session = await req.SessionAsAsync<MySession>();
    props[nameof(MySession.CompanyId)] = session.CompanyId;
    props.PopulateInstance(dto);
});

The issue with this is that if it’s on the Request DTO then the UI is going to reset it to null if it’s submitted with an empty value, which you can prevent by denying it from being reset with:

Plugins.Add(new AutoQueryFeature {
    MaxLimit = 100,
    AutoCrudMetadataFilters = {
        meta => meta.DenyReset.Add(nameof(MySession.CompanyId))
    },
});

Is there anyway for me to debug it to see if the CompanyId is created / used everywhere. My request looks like this now. Seems when I do CreateAccountTypes it auto fills the CompanyId.

However when I do QueryAccountTypes, it brings back all records and not just the account types for my CompanyId linked to my login.

using ServiceStack;
using PowerPropApi.ServiceModel.Types;
[assembly: HostingStartup(typeof(PowerPropApi.ConfigureAutoQuery))]

namespace PowerPropApi
{
    public class ConfigureAutoQuery : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder) => builder
            .ConfigureAppHost(appHost => {
                appHost.Plugins.Add(new AutoQueryFeature {
                    MaxLimit = 1000,
                    IncludeTotal = true,
                    GenerateCrudServices = new GenerateCrudServices {
                        AutoRegister = true,
                        //AddDataContractAttributes = false,
                        ServiceFilter = (op, req) =>
                        {
                            op.Request.AddAttribute(new AutoPopulateAttribute(nameof(CustomUserSession.CompanyId))
                            {
                                Eval = "userSession.CompanyId"
                            });
                            op.Request.Properties.RemoveAll(x => x.Name == nameof(CustomUserSession.CompanyId));
                        }
                    },
                    //AutoCrudMetadataFilters = 
                    //{
                    //     meta => meta.Add(new AutoPopulateAttribute(nameof(AccountType.CompanyId)) {
                    //                Eval = "userSession.CompanyId"
                    //                })
                    //}
                });
            });
    }
}


All my tables uses AutoRegister.  E.g. table is :
create table applicant_type
(
    id          integer generated always as identity
        primary key,
    description varchar(255) not null,
    company_id  bigint
        constraint applicant_type_company_id_fk
            references company
);

AutoPopulateAttribute only populates the model when creating/updating records, there’s no model populated in Query APIs. If you want to ensure queries are filtered you’d use an AutoFilter attribute instead.

For deeper insights into your App’s internals you can enable the new Admin Profiling UI.

The Global Request/Response filters lets you intercept the Request & Responses.

You can also Intercept and Introspect Every AutoQuery with a custom base class.

I have now created my own POCOS and Request DTO’s to test the AutoFilter and AutoPopulate

[Schema("public")]
    public class AccountType
    {
        [Required]
        [PrimaryKey]
        [AutoIncrement]
        [DataMember(Order = 1)]
        public int Id { get; set; }
        [Required]
        [References(typeof(FinancialStatement))]
        [DataMember(Order = 2)]
        public int FinancialStatementId { get; set; }
        [Required]
        [DataMember(Order = 3)]
        public string Description { get; set; }
        [References(typeof(Company))]
        [DataMember(Order = 4)]
        public long? CompanyId { get; set; }
    }



[AutoPopulate(nameof(AccountType.CompanyId), Eval = "`${userSession.CompanyId}`")]
    [Route("/account_type", "POST")]
    [DataContract]
    public class AccountTypeAdd : IReturn<IdResponse>, IPost, ICreateDb<AccountType>
    {
        [DataMember(Order = 2)]
        public int FinancialStatementId { get; set; }
        [DataMember(Order = 3)]
        public string Description { get; set; }
        [DataMember(Order = 4)]
        public long? CompanyId { get; set; }
    }

    [AutoPopulate(nameof(AccountType.CompanyId), Eval = "`${userSession.CompanyId}`")]
    [Route("/account_type/{Id}", "PUT")]
    [DataContract]
    public class AccountTypeEdit : IReturn<IdResponse>, IPut, IUpdateDb<AccountType>
    {
        [DataMember(Order = 1)]
        public int Id { get; set; }

        [DataMember(Order = 2)]
        public int FinancialStatementId { get; set; }

        [DataMember(Order = 3)]
        public string Description { get; set; }

        [DataMember(Order = 4)]
        public long? CompanyId { get; set; }


    }

    [AutoPopulate(nameof(AccountType.CompanyId), Eval = "`${userSession.CompanyId}`")]
    [Route("/account_type/{Id}", "PATCH")]
    [DataContract]
    public class AccountTypePatch : IReturn<IdResponse>, IPatch, IPatchDb<AccountType>
    {

        [DataMember(Order = 1)]
        public int Id { get; set; }

        [DataMember(Order = 2)]
        public int FinancialStatementId { get; set; }

        [DataMember(Order = 3)]
        public string Description { get; set; }

        [DataMember(Order = 4)]
        public long? CompanyId { get; set; }

    }

    [Route("/account_type/{Id}", "DELETE")]
    [DataContract]
    public class AccountTypeDelete : IReturn<IdResponse>, IDelete, IDeleteDb<AccountType>
    {
        [DataMember(Order = 1)]
        public long Id { get; set; }
    }

    [Route("/account_type", "GET")]
    [Route("/account_type/{Id}", "GET")]
    [DataContract]

    [AutoFilter(QueryTerm.Ensure, nameof(AccountType.CompanyId), Eval = "`${userSession.CompanyId}`")]
    //[AutoPopulate(nameof(AccountType.CompanyId), Eval = "`${userSession.CompanyId}`")]
    public class AccountTypeLookup : QueryDb<AccountType>, IReturn<QueryResponse<AccountType>>, IGet
    {
        [DataMember(Order = 1)]
        public long? Id { get; set; }

        [DataMember(Order = 2)]
        public long? CompanyId { get; set; }
    }

When I however try and query the endpoint AccountTypeLookup

I get the following error.

Commenting out the AutoFilter and replacing it with AutoPopulate, work. This seems to be a bug

I want to try and use AutoFilter on my CRUD operations so it will only update the record if Id = the Id and CompanyId = userSeccion.CompanyId to assure you can only update your own tenant’s data

If CompanyId is an int you’re trying to populate and compare it with a string which would cause this issue.

Why is the same thing working with AutoPopulate but no AutoFilter.

On both of them I use nameof(AccountType.CompanyId), Eval = “${userSession.CompanyId}”)]

Don’t use string interpolation for integer types, just reference the property:

Because one is deserialized in code where it’s coerced into the target type, the other goes into a DB param as the wrong type. Either way you shouldn’t be relying on coercion and be using the correct type where ever possible.

Somehow my EndsWithConvention is not working. If I do it in my request DTO’s it work

[QueryDbField(Template = "UPPER({Field}) LIKE UPPER({Value})",
               Field = "TableNamePc", ValueFormat = "%{0}%")]

But setting it globally below does not work.

appHost.Plugins.Add(new AutoQueryFeature {
                    MaxLimit = 1000,
                    IncludeTotal = true,
                    EndsWithConventions = new Dictionary<string, QueryDbFieldAttribute>
                    {
                        { "StartsWith", new QueryDbFieldAttribute {
                              Template= "UPPER({Field}) LIKE UPPER({Value})",
                              ValueFormat= "{0}%" }},
                        { "Contains", new QueryDbFieldAttribute {
                              Template= "UPPER({Field}) LIKE UPPER({Value})",
                              ValueFormat= "%{0}%" }},
                        { "EndsWith", new QueryDbFieldAttribute {
                              Template= "UPPER({Field}) LIKE UPPER({Value})",
                              ValueFormat= "%{0}" }},
                    },

It’s already configured by default:

Why are you redeclaring it?

I wanted to override it to make it UPPER as Postgres search are case sensitive

You can make it case-insensitive by configuring AutoQuery with:

Plugins.Add(new AutoQueryFeature {
    StripUpperInLike = false
});

I have decided to extend CrudEvent and add a CompanyId to it. I would then use a database trigger on the database with my own lookup table to update this CompanyId into the tables directly.

I extended the CrudEvent Table like

[AutoPopulate(nameof(AuditCrudEvent.CompanyId), Eval = “userSession.CompanyId”)]

public class AuditCrudEvent : CrudEvent
{
    public long CompanyId { get; set; }
}

I then in my AppHost did the following

container.AddSingleton(c => new OrmLiteCrudEvents(c.Resolve()));
container.Resolve().InitSchema();

This would therefore mean that any Audits written will include the CompanyId.

When I do any crud operation, the CompanyId is not filled. It stays 0

That’s not how [AutoPopulate] works, which only works on AutoQuery CRUD Services.

OrmLiteCrudEvents is what populates the CrudEvent table, you’d need a custom OrmLiteCrudEvents<T> impl with a custom Record/Async implementations that populates any additional custom fields.

Also the CrudEvent table wasn’t designed to be extended, if you create a custom table you should use [Alias(nameof(CrudEvent))] so it retains the same table name otherwise any query on CrudEvent will fail.

I’d instead recommend making use of the RefId, RefIdStr and Meta dictionary columns on the CrudEvent table which is meant to store custom data which you can populate in OrmLiteCrudEvents.EventFilter.

Thanks. I basically updated the ref_id, ref_id_str and meta on my AppUser table as you suggested and reverted back to the standard CrudEvents without an override. Saved data using https://localhost:5001/ui/PatchAccountType (which is one of my tables set up with AutoCrud)

Crud record is created but the ref_id, ref_id_str and meta is null in the CrudEvents Table even though it is set in AppUser table.