AutoCRUD Preview

Happy to announce a preview of a new “Auto CRUD” feature that will be in the next release that I’d welcome any early feedback on. It’s currently available in the latest v5.8.1 on MyGet.

It’s conceptually the same as “Auto Query” where you just need to implement the Request DTOs definition for your DB Table APIs and AutoQuery automatically provides the implementation for the Service. You’ll need to register the AutoQueryFeature plugin to enable this feature, e.g:

Plugins.Add(new AutoQueryFeature { 
    MaxLimit = 100 
});

There’ll be documentation when released, but currently the best way to explore the available features is looking at the tests in AutoQueryCrudTests.cs.

Essentially the way it works is that you’ll define Request DTOs that implement one of the following interfaces which dictates the behavior of the Service:

  • ICreateDb<Table> - Create new Table Entry
  • IUpdateDb<Table> - Update existing Table Entry
  • IDeleteDb<Table> - Delete existing Table Entry

All Request DTOs also require either an IReturn<T> or IReturnVoid marker interface to specify the return type of the Service. Can also use IReturn<EmptyResponse> for an “empty” response where as IReturnVoid returns “no” response.

I’ll go through a simple example, supposing we have a simple POCO table we want to maintain:

public class Rockstar
{
    [AutoIncrement]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
    public DateTime DateOfBirth { get; set; }
    public DateTime? DateDied { get; set; }
    public LivingStatus LivingStatus { get; set; }
}

We can create a Service that inserts new Rockstar by defining all the properties we want to allow API consumers to provide when creating a new Rockstar:

public class CreateRockstar : ICreateDb<Rockstar>,IReturn<CreateRockstarResponse> 
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
    public DateTime DateOfBirth { get; set; }
}

public class CreateRockstarResponse
{
    public int Id { get; set; } // Id is auto populated with RDBMS generated Id
    public ResponseStatus ResponseStatus { get; set; }
}

You can now insert Rockstars by calling this API:

client.Post(new CreateRockstar {
    FirstName = "Kurt",
    LastName = "Cobain",
    Age = 27,
    DateOfBirth = new DateTime(20,2,1967),
});

Similarly you can define “Update” and “Delete” Services the same way, e.g:

public class UpdateRockstar : Rockstar,
    IUpdateDb<Rockstar>, IReturn<UpdateRockstarResponse> {}

public class UpdateRockstarResponse
{
    public int Id { get; set; } // Id is auto populated with RDBMS generated Id
    public Rockstar Result { get; set; } // selects & returns latest DB Rockstar
    public ResponseStatus ResponseStatus { get; set; }
}

If your Response DTO contains any of these properties it will be populated by AutoQuery:

  • T Id - The Primary Key
  • T Result - The POCO you want to return (can be a subset of DB model)
  • int Count - Return the number of rows affected (Delete’s can have >1)

Delete Services need only a Primary Key, e.g:

public class DeleteRockstar : IDeleteDb<Rockstar>, IReturnVoid 
{
    public int Id { get; set; }
}

and to Query the Rockstar table you have the full featureset of AutoQuery for a complete set of CRUD Services without needing to provide any implementation.

Advanced CRUD Example

I’ll now go through a more advanced example that implements Audit information as well as layered support for multi-tenancy to see how you can compose features.

So lets say you have an interface that all tables you want to contain Audit information implements:

public interface IAudit 
{
    DateTime CreatedDate { get; set; }
    string CreatedBy { get; set; }
    string CreatedInfo { get; set; }
    DateTime ModifiedDate { get; set; }
    string ModifiedBy { get; set; }
    string ModifiedInfo { get; set; }
    DateTime? SoftDeletedDate { get; set; }
    string SoftDeletedBy { get; set; }
    string SoftDeletedInfo { get; set; }
}

Optional, but it’s also useful to have a concrete base table form OrmLite which I would annotate like:

public abstract class AuditBase : IAudit
{
    public DateTime CreatedDate { get; set; }
    [Required]
    public string CreatedBy { get; set; }
    [Required]
    public string CreatedInfo { get; set; }

    public DateTime ModifiedDate { get; set; }
    [Required]
    public string ModifiedBy { get; set; }
    [Required]
    public string ModifiedInfo { get; set; }

    [Index] //Check if Deleted
    public DateTime? SoftDeletedDate { get; set; }
    public string SoftDeletedBy { get; set; }
    public string SoftDeletedInfo { get; set; }
}

We can then create a base Request DTO that all Audit Create Services will implement:

[ValidateRequest("IsAuthenticated")]
[AutoPopulate(nameof(IAudit.CreatedDate),  Eval = "utcNow")]
[AutoPopulate(nameof(IAudit.CreatedBy),    Eval = "userAuthName")] //or userAuthId
[AutoPopulate(nameof(IAudit.CreatedInfo),  Eval = "`${userSession.DisplayName} (${userSession.City})`")]
[AutoPopulate(nameof(IAudit.ModifiedDate), Eval = "utcNow")]
[AutoPopulate(nameof(IAudit.ModifiedBy),   Eval = "userAuthName")] //or userAuthId
[AutoPopulate(nameof(IAudit.ModifiedInfo), Eval = "`${userSession.DisplayName} (${userSession.City})`")]
public abstract class CreateAuditBase<Table,TResponse> : ICreateDb<Table>, IReturn<TResponse> {}

These all call #Script Methods which you can add/extend yourself.

The *Info examples is a superflous example showing that you can basically evaluate any #Script expression. Typically you’d only save User Id or Username

The [AutoPopulate] attribute tells AutoCrud that you want the DB Table to automatically populate these properties. They’re are 3 different

  • Value - A constant value that can be used in C# Attributes, e.g Value="Foo"
  • Expression - A Lightweight #Script Expression which is only evaluated once and cached globally, e.g. Expression = "date(2001,1,1)", useful for values that can’t be defined in C# Attributes like DateTime, can be any #Script Method.
  • Eval - A #Script Expression that’s cached per request. E.g. Eval="utcNow" calls the utcNow Script method which returns DateTime.UtcNow which is cached for that request so all other utcNow expressions will return the same exact value.
  • NoCache - Don’t cache the expression, evaluate it each time

The AST is cached for all #Script expressions used in the AutoCrud so it’s still fast to evaluate even when results are not cached.

Now lets say we want to layer on additional generic functionality, we can inherit the base class and extend it with additional functionality, e.g. if we want our table to support Multitenancy we could extend it with:

[AutoPopulate(nameof(IAuditTenant.TenantId), Eval = "Request.Items.TenantId")]
public abstract class CreateAuditTenantBase<Table,TResponse> 
    : CreateAuditBase<Table,TResponse> {}

Where TenantId is added in a Global Request Filter, e.g. after inspecting the authenticated UserSession to determine which tenant they belong to.

Anyway we can now easily implement custom “Audited” and “Multi Tenant” CRUD Services by inheriting these base Services, e.g: our custom Table that implements our AuditBase class with a TenantId to capture the Tenant the record is in:

public class RockstarAuditTenant : AuditBase
{
    [Index]
    public int TenantId { get; set; }
    [AutoIncrement]
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
    public DateTime DateOfBirth { get; set; }
    public DateTime? DateDied { get; set; }
    public LivingStatus LivingStatus { get; set; }
}

Our service can now implement our base Audit & Multitenant enabled service:

public class CreateRockstarAuditTenant 
    : CreateAuditTenantBase<RockstarAuditTenant, CreateRockstarResponse>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int? Age { get; set; }
    public DateTime DateOfBirth { get; set; }
}

And all the decorated properties will be automatically populated when creating the Rockstar.

Here are the generic and concrete services used for Updates:

[ValidateRequest("IsAuthenticated")]
[AutoPopulate(nameof(IAudit.ModifiedDate), Eval = "utcNow")]
[AutoPopulate(nameof(IAudit.ModifiedBy),   Eval = "userAuthName")] //or userAuthId
[AutoPopulate(nameof(IAudit.ModifiedInfo), Eval = "`${userSession.DisplayName} (${userSession.City})`")]
public abstract class UpdateAuditBase<Table,TResponse> 
    : IUpdateDb<Table>, IReturn<TResponse> {}

[AutoFilter(nameof(IAuditTenant.TenantId), Eval="Request.Items.TenantId")]
public abstract class UpdateAuditTenantBase<Table,TResponse> 
    : UpdateAuditBase<Table,TResponse> {}

public class UpdateRockstarAuditTenant 
    : UpdateAuditTenantBase<RockstarAuditTenant, RockstarWithIdResponse>
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public LivingStatus? LivingStatus { get; set; }
}

Note that the [AutoPopulate] properties only appear on the DB Table, not the Request DTO since we don’t want external API consumers to populate them.

Here are the base & concrete Services for Soft Deletes which is an UPDATE behind-the-scenes to populate the SoftDelete* fields:

[ValidateRequest("IsAuthenticated")]
[AutoPopulate(nameof(IAudit.SoftDeletedDate), Eval = "utcNow")]
[AutoPopulate(nameof(IAudit.SoftDeletedBy),   Eval = "userAuthName")] //or userAuthId
[AutoPopulate(nameof(IAudit.SoftDeletedInfo), Eval = "`${userSession.DisplayName} (${userSession.City})`")]
public abstract class SoftDeleteAuditBase<Table,TResponse> 
    : IUpdateDb<Table>, IReturn<TResponse> {}

[AutoFilter(QueryTerm.Ensure, nameof(IAuditTenant.TenantId),  Eval = "Request.Items.TenantId")]
public abstract class SoftDeleteAuditTenantBase<Table,TResponse> 
    : SoftDeleteAuditBase<Table,TResponse> {}

public class SoftDeleteAuditTenant 
    : SoftDeleteAuditTenantBase<RockstarAuditTenant, RockstarWithIdResponse>
{
    public int Id { get; set; }
}

Some Apps prefer to never delete fields and instead mark records as deleted so it leaves an audit trail. Here’s an example of a “Real” DELETE as well:

[ValidateRequest("IsAuthenticated")]
[AutoFilter(QueryTerm.Ensure, nameof(IAuditTenant.TenantId),  Eval = "Request.Items.TenantId")]
public class RealDeleteAuditTenant 
    : IDeleteDb<RockstarAuditTenant>, IReturn<RockstarWithIdResponse>
{
    public int Id { get; set; }
    public int? Age { get; set; }
}

Now if you’re creating Soft Delete & Multi tenant services you’ll want to ensure that every query doesn’t return deleted items and only records in their tenant, which we can implement with:

[ValidateRequest("IsAuthenticated")]
[AutoFilter(QueryTerm.Ensure, nameof(IAudit.SoftDeletedDate), Template = SqlTemplate.IsNull)]
[AutoFilter(QueryTerm.Ensure, nameof(IAuditTenant.TenantId),  Eval = "Request.Items.TenantId")]
public abstract class QueryDbTenant<From, Into> : QueryDb<From, Into> {}

The [AutoFilter] lets you add pre-configured filters to the query, Ensure is a new OrmLite feature which basically forces always applying this filter, even if the query contains other OR conditions.

This base class will then let you create concrete queries that doesn’t return soft deleted rows and only returns rows from the same tenant as the authenticated user, e.g:

public class QueryRockstarAudit : QueryDbTenant<RockstarAuditTenant, Rockstar>
{
    public int? Id { get; set; }
}

To coincide with AutoCRUD there’s also support for declarative validation which thanks to #Script lets you define your Fluent Validation Rules by annotating your Request DTO properties. As it’s essentially a different way to define Fluent Validation Rules, it still needs Validation enabled to run:

Plugins.Add(new ValidationFeature());

Refer to AutoQueryCrudTests.cs for other features, e.g. you can use [AutoMap] to map DTO properties to DB Table properties with a different name.

You can use AutoMapping’s AutoPopulate API to intercept the mapping between Request DTO and the Object Dictionary containing the fields that are persisted. It also supports Auto Guid properties and Optimistic Concurrency using OrmLite’s RowVersion support.

Happy for any feedback and to answer any questions on this Thread.

6 Likes

Hi i’m trying this new feature :slight_smile: i have found an issue with update creation of new records work fine.
It seem it is crashing when trying to generate the response, in fact the record on database is updated correctly.

I have made a quick dirt sample :slight_smile:

https://github.com/nukedbit/ssupdatecrash 

It gives an error either by calling the generated end point or by using the gateway to call the service internally (which i’m using on my real app).

you can call it with postman

POST https://localhost:5001/json/reply/UpdatePersonGateway

{
	"id":2,
	"name":"marco"
}

You can add a breakpoint on Requests.cs line 104

Thanks for trying it out! I’m getting a 404 from https://github.com/nukedbit/ssupdatecrash is it a private repo?

Sorry yes it was private now is public

Cool, nice catch, forgot an await in the IUpdateDb<T> implementation, resolved in this commit, thx!

This change is available from the latest v5.8.1 that’s now available on MyGet. As you already have v5.8.1 installed you’ll have to clear your NuGet packages cache to fetch the latest version.

The current issue with your example now is that your Person POCO have the properties that your UpdateAuditBase<T> is trying to update since it doesn’t inherit AuditBase but when it does it wont be able to populate the fields since you’re not Authenticated so your *AuditBase<T> Services should be annotated with [Authenticate] attribute so it only allows access by Authenticated Users.

Oh yeah sorry i missed that when making the example i have just checked on my project and it works fine now.

BTW This is a great addition to servicestack lot of times i had simple crud services and this same me a lot of work. The design is very nice, and thanks to auto mapping and sharpscript integration on validation i think is going to be a very powerful addition.

Thanks again for the fast support.

Great happy to hear it, yeah I think the AutoCRUD support has been a long time coming and should complement AutoQuery nicely with its familiar approach/dev model.

Yeah I’m looking forward to the declarative Validation support as well, thanks to the flexibility and expressiveness of #Script it was able to be implemented to seamlessly to work within the existing integrated Fluent Validation support.

Being able to mix Validation Rules defined fluently in C#, declaratively with #Script and from a dynamic RDBMS Table source should provide a lot of flexibility, which will support use-cases like Business Users being able to add and update validation rules in a RDBMS at runtime and have it be instantly applied.

The nice thing about the declarative validation not having coupling to implementation logic is that we can publish it in metadata to clients so it can better express what’s required by API consumers. And because #Script uses JS Expressions we’ll eventually be able to reuse the same server validation rules and execute them in client browsers for the best UX, although it would need to be limited to use normal JS methods instead of #Script extension method support, e.g:

public class MyRequest
{
    [Validate("NotNull")]
    public string Name { get; set; }

    [Validate(Condition = "it.isOdd() && it.log() > 2")]
    //[Validate(Condition = "isOdd(it) && log(it) > 2")] // JS Compatible
    public int IsOddAndOver2Digits { get; set; }
}

But when JS gets |> support implemented in browsers you’ll be able to execute the more readable version of:

[Validate(Condition = "it |> isOdd")]
public int IsOdd { get; set; }

BTW I’ve just deployed better error messages in latest v5.8.1 so if you try to update a field that doesn’t exist the client will receive an error message like:

‘ModifiedDate’ is not a property of ‘Person’

BTW thanks for the feedback, happy to resolve an issues before the next release or look into adding any additional features you think it should have.

Yeah this are all great addition, #script on these context add great value in term of flexibility.
One thing just come up to my mind, basically on my project i have managed to get blazor server side working with SS, and also managed to use SS authentication in blazor instead of asp.net one.
This way i get to call SS Gateway internally and i also get endpoints for mobile for free.
I also integrated this fluent validator integration for blazor https://github.com/ryanelian/FluentValidation.Blazor replacing official fluent with SS one.
Do you think it would be possible to use SS new declarative validition on EditForm basically what this component need it is to give him an IValidator

1 Like

Definitely interested in hearing more about your SS + Blazor integration, if you have a basic showcase of it somewhere maybe we can turn it into a project template.

If you’ve got it working source-compatible with SS Fluent Validation than it should work in theory. Basically the way declarative validation works is that the [Validate({Validator}] expression calls the Fluent Validation scripts in ValidateScripts.cs which returns a FV IPropertyValidator which is combined together into FV IValidationRule rules which are added to the Validator when it’s created in AbstractValidator base class.

Whereas the inline #Script Conditions are just registering a ScriptConditionValidator so basically everything is still working within FV’s development model, we’re just by-passing their Fluent Typed APIs to add the rules to the validator.

Good. are the validator availabe from Ioc? I could get an IValidator?

BTW i have just completed an example project with blazor integration

1 Like

Yeah every validator is registered as IValidator<T> in the IoC, if you only have the runtime Type you can resolve it using the ValidatorCache.GetValidator(IRequest,Type) wrapper.

Great thanks for publishing it on GitHub, will check it out.

Trying this out.

The OrmLiteConfig.InsertFilter = (dbCmd, row) => {} has a row type of Dictionary<string, object> I believe. Should we not count on using this for these auto crud APIs, as it’s hard to distinguish the object types if they are all dictionaries?

I also noticed that when I add the “Id” field to the response and the Id is a Guid, it gives a “FormatException”. If I remove the Id from the response, there’s no exception (only on ICreateDb<>). Also, the Result property is empty when I use the ICreateDb<>, but it does get populated for the IUpdateDb<>. Is it possible to get that populated on create IF the property exists, to avoid another call (as many properties get set automatically on the server)? In all circumstances, it seems like the database records were created nonetheless. I was able to use the AutoPopulate attribute with a couple custom ScriptMethods too, pretty cool.

The QueryDb<> implementation also lets you create service methods to override the implementation to add functionality, will that be available (or on the roadmap for the future) for the crud class implementations as well?

This is a really great new feature.

Yeah the Auto CRUD APIs are using the Object Dictionary APIs which is needed in order to support the flexibility/features needed. I’ve just updated OrmLite to wrap any anonymous rows (e.g. Dictionary or anon object) into an IDynamicRow wrapper which can be inspected with something like:

OrmLiteConfig.InsertFilter = (dbCmd, row) => {
    if (row is IDynamicRow drow)
    {
        if (ShouldLog(drow.Type))
        {
            Log(drow switch {
                IDynamicRow<Dictionary<string,object>> mapRow => mapRow.Fields,
                IDynamicRow<object> objRow =>                    objRow.Fields,
            });
        }        
    }
    else { ... } //normal T row
};

Can you show me the DTOs you’re using to test the invalid Guid response?

Yeah it’s still a regular ServiceStack Service which you can override and provide your own custom implementation and take over the implementation just like in AutoQuery, you can then either handle the CRUD operation yourself or call the new CRUD APIs on IAutoQueryDb (now in latest v5.8.1).

  • The IDynamicRow lets me discover the type, so that should work.
  • Regarding being able to leverage it in a service, sweet!
  • The DTO basically looks like (couple notes in there with my comments):
public class DaoBase
{
        public virtual Guid Id { get; set; }
        public virtual DateTimeOffset CreateDate { get; set; }
        public virtual string CreatedBy { get; set; }
        public virtual DateTimeOffset ModifiedDate { get; set; }
        public virtual string ModifiedBy { get; set; }
}
public class Bookmark: DaoBase
{
        public string Slug { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string Url { get; set; }
} 
public class QueryBookmarks : QueryDb<Bookmark> { }

// custom script methods
[AutoPopulate(nameof(Bookmark.Id), Eval = "newGuid")] 
[AutoPopulate(nameof(Bookmark.CreatedBy), Eval = "currentUserIdentifier")]
[AutoPopulate(nameof(Bookmark.CreateDate), Eval = "utcNowDateTimeOffset")]
[AutoPopulate(nameof(Bookmark.ModifiedBy), Eval = "currentUserIdentifier")]
[AutoPopulate(nameof(Bookmark.ModifiedDate), Eval = "utcNowDateTimeOffset")]
public class CreateBookmark : ICreateDb<Bookmark>, IReturn<CreateBookmarkResponse>
{
        public string Slug { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public string Url { get; set; }
}
public class CreateBookmarkResponse
{
        // NOTE: Uncommenting this causes a FormatException
        // public Guid Id { get; set; }
        // NOTE: This Result prop does not get populated, if the property is 
        // on the response type, it would be nice for it to be populated
        public Bookmark Result { get; set; }
        public ResponseStatus ResponseStatus { get; set; }
}

You can use [AutoId] to auto populate Guids

public class DaoBase
{
    [AutoId]
    public virtual Guid Id { get; set; }
}

newGuid isn’t a built-in Script Method, is it a script method you’ve registered? If you’re not registering your own methods in SharpPagesFeature you can add them to AppHost.ScriptContext which falls back to a default ScriptContext for the AppHost.

You can create a new instance of types with the new .NET Scripting Method, e.g. 'Guid'.new() but that’s going to return an empty guid to generate a new Guid you’ll need to use the Function APIs, e.g. F('Guid.NewGuid')().

Yes, it’s a custom script method I built, and it works, it populates the Id nicely, in fact the record does get created in the database as desired, it just looks like the error happens when it tries to format and return the response.

1 Like

ok cool, I’ll see if I can repro the issue.

It looks like the [AutoId] doesn’t incur the error, and it populates both the Id and the Result prop.
Without it, and going back to the AutoPopulate method, the error looks like this:

{
  "__type": "ServiceStack.ErrorResponse, ServiceStack.Interfaces",
  "responseStatus": {
    "__type": "ServiceStack.ResponseStatus, ServiceStack.Interfaces",
    "errorCode": "FormatException",
    "message": "Input string was not in a correct format.",
    "stackTrace": "   at ServiceStack.Text.DefaultMemory.ParseGeneralStyleGuid(ReadOnlySpan`1 value, Int32& len) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\DefaultMemory.cs:line 639\r\n   at ServiceStack.Text.DefaultMemory.ParseGuid(ReadOnlySpan`1 value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\DefaultMemory.cs:line 415\r\n   at ServiceStack.Text.Common.DeserializeBuiltin`1.<>c.<GetParseStringSpanFn>b__7_6(ReadOnlySpan`1 value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Common\\DeserializeBuiltin.cs:line 72\r\n   at ServiceStack.Text.Common.JsReader`1.<>c__DisplayClass4_0`1.<GetCoreParseStringSpanFn>b__3(ReadOnlySpan`1 value) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Common\\JsReader.cs:line 70\r\n   at ServiceStack.Text.Jsv.JsvReader.<>c__DisplayClass2_0.<GetParseFn>b__0(String v) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\Jsv\\JsvReader.Generic.cs:line 18\r\n   at ServiceStack.Text.TypeSerializer.DeserializeFromString(String value, Type type) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\TypeSerializer.cs:line 89\r\n   at ServiceStack.AutoMappingUtils.ChangeValueType(Object from, Type toType) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\AutoMappingUtils.cs:line 320\r\n   at ServiceStack.AutoMappingUtils.ConvertTo(Object from, Type toType, Boolean skipConverters) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\AutoMappingUtils.cs:line 158\r\n   at ServiceStack.AutoMappingUtils.ConvertTo(Object from, Type toType) in C:\\BuildAgent\\work\\912418dcce86a188\\src\\ServiceStack.Text\\AutoMappingUtils.cs:line 137\r\n   at ServiceStack.AutoQuery.ExecAndReturnResponseAsync[Table](Object dto, IDbConnection db, Func`2 fn) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack.Server\\AutoQueryFeature.AutoCrud.cs:line 166\r\n   at ServiceStack.AutoQuery.Create[Table](ICreateDb`1 dto, IRequest req) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack.Server\\AutoQueryFeature.AutoCrud.cs:line 28\r\n   at ServiceStack.Host.Handlers.ServiceStackHandlerBase.HandleResponse(IRequest httpReq, IResponse httpRes, Object response) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\Host\\Handlers\\ServiceStackHandlerBase.cs:line 81\r\n   at ServiceStack.Host.RestHandler.ProcessRequestAsync(IRequest req, IResponse httpRes, String operationName) in C:\\BuildAgent\\work\\3481147c480f4a2f\\src\\ServiceStack\\Host\\RestHandler.cs:line 103"
  }
}
1 Like

This should now be resolved from this commit, also I’ve added nowOffset and utcNowOffset to return DateTimeOffset, so you can now create a bookmark with a user-specified Guid + Audit Info returning DateTimeOffset with:

[AutoPopulate(nameof(Bookmark.Id), Eval = "F('Guid.NewGuid')()")] 
[AutoPopulate(nameof(Bookmark.CreatedBy), Eval = "userAuthId")]
[AutoPopulate(nameof(Bookmark.CreateDate), Eval = "utcNowOffset")]
[AutoPopulate(nameof(Bookmark.ModifiedBy), Eval = "userAuthId")]
[AutoPopulate(nameof(Bookmark.ModifiedDate), Eval = "utcNowOffset")]
public class CreateBookmark : ICreateDb<Bookmark>, IReturn<CreateBookmarkResponse>

This release also added support for top-level [ValidateRequest] custom validators which act like pre-conditions where it will fail fast on the first invalid condition, before any of the FluentValidation PropertyValidators are executed.:

[ValidateRequest(Conditions = new[]{ "it.Test.isOdd()", "it.Test.log10() > 2" }, 
    "RuleMessage")]
[ValidateRequest(Condition = "it.Test.log10() > 3", 
    ErrorCode="AssertFailed2", Message="2nd Assert Failed", StatusCode = 401)]
public class OnlyValidatesRequest
    : ICreateDb<RockstarAuto>, IReturn<RockstarWithIdResponse>
{
    // Combined typed conditions + Error code
    public int Test { get; set; }

    [Validate("NotNull")] //doesn't get validated if ValidateRequest is invalid
    public string NotNull { get; set; }
}

This fix is now available on MyGet, you’ll need to clear your NuGet packages cache to download the latest v5.8.1.

1 Like