Say you had a simple object as follows:
public class PaymentLineItem
{
public DateTime TimeStamp { get; set; }
public decimal Amount { get; set; }
public string Description { get; set; }
public PayCodeDto PayCode { get; set; } //= PayCodeDto.Invalid;
public override string ToString()
{
return StringUtils.ToString(this);
}
public override bool Equals(object obj)
{
return Equals(obj as PaymentLineItem);
}
public override int GetHashCode()
{
unchecked
{
var hashCode = TimeStamp.GetHashCode();
hashCode = (hashCode * 397) ^ Amount.GetHashCode();
hashCode = (hashCode * 397) ^ (Description != null ? Description.GetHashCode() : 0);
hashCode = (hashCode * 397) ^ (PayCode != null ? PayCode.GetHashCode() : 0);
return hashCode;
}
}
public bool Equals(PaymentLineItem other)
{
if (other == null) return false;
if (ReferenceEquals(this, other)) return true;
return TimeStamp.Equals(other.TimeStamp)
&& Amount.Equals(other.Amount)
&& string.Equals(Description, other.Description)
&& Object.Equals(PayCode, other.PayCode);
}
}
There is nothing special with this class apart from usage of the new C# auto-property initialize introduced in C# 6.0. PayCodeDto is simple with three properties and implements IEquatable.
If you create a collection of these payment items and serialize this using
var serialized = JsonConvert.SerializeObject(sut);
and you then take the correctly serialized string and deserialize using
var deserialized = JsonConvert.DeserializeObject<List<PaymentLineItemLight>>(serialized);
what you end up with is a collection all instances of the line items having the same PayCode.
This is the sample test:
[Test]
public void Should_Serialize_Collection_In_Package_Correctly()
{
var sut = new List<PaymentLineItemLight>
{
new PaymentLineItemLight
{
Amount = new decimal(153.0000),
Description = "48385 - Multiple items payment",
TimeStamp = DateTime.Parse("2018-01-30T10:40:47.477"),
PayCode = new PayCodeDto("105", "dont-care") {DictionaryId = 2}
},
new PaymentLineItemLight
{
Amount = new decimal(53.0000),
Description = "483816 - Multiple items payment",
TimeStamp = DateTime.Parse("2018-01-30T10:40:47.477"),
PayCode = new PayCodeDto("104", "dont-care") {DictionaryId = 2}
},
new PaymentLineItemLight
{
Amount = new decimal(200.0000),
Description = "483817 - Multiple items payment",
TimeStamp = DateTime.Parse("2018-01-30T10:40:47.477"),
PayCode = new PayCodeDto("102", "dont-care-102") {DictionaryId = 2}
}
};
var serialized = JsonConvert.SerializeObject(sut);
var deserialized = JsonConvert.DeserializeObject<List<PaymentLineItemLight>>(serialized);
CollectionAssert.AreEqual(sut, deserialized);
}
If I remove the auto-property initialize, the test passes. I must be missing something here. Anyone seen this?
Related
I allready look out everything of that problem but none of them worked for me.Everyone is suggestion to use AsNoTracking() for that problem but its has no sense with my problem because im not updating the data which i call from my database.
I have company profile update modal, this company can have profile photo or not but either way i need to update those informations. So that's why i need to control is comapny creating a photo or updating a photo. Let me show u to my code in the bellow :
#region /*UpdateCompanyProfile*/
[HttpPost]
public IActionResult UpdateCompanyProfile(Company company, List<IFormFile> files, int FileID)
{
try
{
if (ModelState.IsValid)
{
company.ModifiedDate = DateTime.Now;
_unitOfWorkC.RepositoryCompany.Update(company);
int firstRequest = HttpContext.Response.StatusCode;
if (firstRequest == 200)
{
_unitOfWorkC.Complete();
if (files.Count != 0)
{
var File = _fileUploader.FileUploadToDatabase(files);
var FileResult = File.Result;
FileResult.CompanyID = company.CompanyID;
if (FileID == 0)//That's the point where i control that file, is it gonna be update or create.
{
_unitOfWorkFR.RepositoryFileRepo.Create(FileResult);
int secondRequest1 = HttpContext.Response.StatusCode;
if (secondRequest1 == 200)
{
int tryCatch = _unitOfWorkFR.Complete();
if (tryCatch != 15)
{
TempData["JS"] = "showSuccess();";
}
else
{
TempData["JS"] = "showError();";
}
}
}
else
{
FileResult.FileID = FileID;
_unitOfWorkFR.RepositoryFileRepo.Update(FileResult); //That's the point where i get the error.
int secondRequest2 = HttpContext.Response.StatusCode;
if (secondRequest2 == 200)
{
int tryCatch2 = _unitOfWorkFR.Complete();
if (tryCatch2 != 15)
{
TempData["JS"] = "showSuccess();";
}
else
{
TempData["JS"] = "showError();";
}
}
else
{
TempData["JS"] = "showError();";
}
}
}
}
else
{
TempData["Message"] = "?irket g?ncelleme i?leminiz ba?ar?s?z!";
TempData["JS"] = "showError();";
return RedirectToAction("CompanyProfile");
}
}
else
{
TempData["Message"] = "G??ncellemek istedi?iniz veri hatal?!";
TempData["JS"] = "showError();";
return RedirectToAction("CompanyProfile");
}
}
catch (Exception ex)
{
var log = _logging.Logging(ex.Message, "Exception/Hata", company.CompanyID.ToString(),
"CompanyProfile/UpdateCompanyProfile", getCurrentUser(), getCurrentUserClaimRole());
_unitOfWorkLog.RepositoryLog.Create(log);
_unitOfWorkLog.Complete();
//TempData["Message"] = ex.Message;
//TempData["JS"] = "showError();";
return RedirectToAction("CompanyProfile");
}
}
#endregion
As u can see, calling that data with AsNoTracking() has no sense in my stuation. I'm only getting that error in that action,so other FileRepo actions are working well.
That's my FileUploadToDatabase() method :
public async Task<FileRepo> FileUploadToDatabase(List<IFormFile> files)
{
foreach (var file in files)
{
var fileName = Path.GetFileNameWithoutExtension(file.FileName);
var fileExtension = Path.GetExtension(file.FileName);
_fileRepo = new FileRepo
{
FileName = fileName,
FileExtension = fileExtension,
FileType = file.ContentType,
CreatedDate= DateTime.Now
};
using (var dataStream = new MemoryStream())
{
await file.CopyToAsync(dataStream);
_fileRepo.FileData = dataStream.ToArray();
}
}
return _fileRepo;
}
And that's my FileRepo class :
public class FileRepo : Base
{
[Key]
public int FileID { get; set; }
[Required(ErrorMessage = "Required Field !")]
public string FileName { get; set; }
[Required(ErrorMessage = "Required Field !")]
public string FileType { get; set; }
[Required(ErrorMessage = "Required Field !")]
public string FileExtension { get; set; }
public string FilePath { get; set; }
public bool FilePhotoIsDefault { get; set; }
public byte[] FileData { get; set; }
public int? CompanyID { get; set; }
public Company Company { get; set; }
#endregion
}
That's my UnitOfWork :
and This is my Repository :
This is the query for my Update Modal:
public IEnumerable<Company> GetByIDForCompanyProfileCompany(int ID)
{
return TradeTurkDBContext.Companies.Where(x => x.CompanyID == ID)
.Include(x => x.Addresses.Where(x => x.IsDeleted == null || x.IsDeleted == false))
//
.Include(x => x.Products.Where(x => x.IsDeleted == null || x.IsDeleted == false))
.ThenInclude(x => x.FileRepos.Where(x => x.IsDeleted == null || x.IsDeleted == false)).AsSplitQuery()
//
.AsNoTrackingWithIdentityResolution().ToList();
}
For updating FileResult you are using DbSet.Update - it is trying to attach entity to ChangeTracker. Attaching will fail if there is already attached object with the same key.
Change your repository to the following. It will update all fields if entity is not in ChangeTracker, otherwise it will correct only needed properties:
public void Update(T model)
{
if (model == null)
throw new ArgumentNullException(nameof(model));
// I hope your generic repository knows Model Id property
var entry = _context.ChangeTracker.Entries<T>().FirstOrDefault(e => e.Entity.Id == model.Id);
if (entry == null)
{
// entity not tracked, so attach it
_dbSet.Update(model);
}
else
{
// setting values from not tracked object
if (!ReferenceEquals(model, entry.Entity))
entry.CurrentValues.SetValues(model);
}
}
UPDATE
If generic repository don't know about Id property, you can define interface for that:
public interface IEntityWithId
{
int Id {get;}
}
Make sure that your classes is implementation of IEntityWithId. Then correct Repository definition:
public interface IRepository<T> where T: class, IEntityWithId
{
...
}
We have an interface and a base class with multiple derived types.
public interface IEvent
{
[JsonProperty("id")]
public string Id { get; set; }
string Type { get; }
}
public abstract class EventBase: IEvent
{
public string Id { get; set; }
public abstract string Type { get; }
}
public class UserCreated : EventBase
{
public override string Type { get; } = typeof(UserCreated).AssemblyQualifiedName;
}
public class UserUpdated : EventBase
{
public override string Type { get; } = typeof(UserUpdated).AssemblyQualifiedName;
}
We are storing these events of different derived types in the same container in Cosmos DB using v3 of .Net SDK Microsoft.Azure.Cosmos. We then want to read all the events and have them deserialized to the correct type.
public class CosmosDbTests
{
[Fact]
public async Task TestFetchingDerivedTypes()
{
var endpoint = "";
var authKey = "";
var databaseId ="";
var containerId="";
var client = new CosmosClient(endpoint, authKey);
var container = client.GetContainer(databaseId, containerId);
await container.CreateItemAsync(new UserCreated{ Id = Guid.NewGuid().ToString() });
await container.CreateItemAsync(new UserUpdated{ Id = Guid.NewGuid().ToString() });
var queryable = container.GetItemLinqQueryable<IEvent>();
var query = queryable.ToFeedIterator();
var list = new List<IEvent>();
while (query.HasMoreResults)
{
list.AddRange(await query.ReadNextAsync());
}
Assert.NotEmpty(list);
}
}
Doesn't seem to be any option to tell GetItemLinqQueryable how to handle types. Is there any other method or approach to support multiple derived types in one query?
It's ok to put the events in some kind of wrapper entity if that would help, but they aren't allowed to be stored as an serialized sting inside a property.
The comment from Stephen Clearly pointed me in the right direction and with the help of this blog https://thomaslevesque.com/2019/10/15/handling-type-hierarchies-in-cosmos-db-part-2/ I ended up with a solution similar to the following example were we have a custom CosmosSerializer that uses a custom JsonConverter that reads the Type property.
public interface IEvent
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("$type")]
string Type { get; }
}
public abstract class EventBase: IEvent
{
public string Id { get; set; }
public string Type => GetType().AssemblyQualifiedName;
}
public class UserCreated : EventBase
{
}
public class UserUpdated : EventBase
{
}
EventJsonConverter reads the Type property.
public class EventJsonConverter : JsonConverter
{
// This converter handles only deserialization, not serialization.
public override bool CanRead => true;
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
// Only if the target type is the abstract base class
return objectType == typeof(IEvent);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// First, just read the JSON as a JObject
var obj = JObject.Load(reader);
// Then look at the $type property:
var typeName = obj["$type"]?.Value<string>();
return typeName == null ? null : obj.ToObject(Type.GetType(typeName), serializer);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException("This converter handles only deserialization, not serialization.");
}
}
The NewtonsoftJsonCosmosSerializer takes a JsonSerializerSettings that it uses for serialization.
public class NewtonsoftJsonCosmosSerializer : CosmosSerializer
{
private static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true);
private readonly JsonSerializer _serializer;
public NewtonsoftJsonCosmosSerializer(JsonSerializerSettings settings)
{
_serializer = JsonSerializer.Create(settings);
}
public override T FromStream<T>(Stream stream)
{
if (typeof(Stream).IsAssignableFrom(typeof(T)))
{
return (T)(object)stream;
}
using var sr = new StreamReader(stream);
using var jsonTextReader = new JsonTextReader(sr);
return _serializer.Deserialize<T>(jsonTextReader);
}
public override Stream ToStream<T>(T input)
{
var streamPayload = new MemoryStream();
using var streamWriter = new StreamWriter(streamPayload, encoding: DefaultEncoding, bufferSize: 1024, leaveOpen: true);
using JsonWriter writer = new JsonTextWriter(streamWriter);
writer.Formatting = _serializer.Formatting;
_serializer.Serialize(writer, input);
writer.Flush();
streamWriter.Flush();
streamPayload.Position = 0;
return streamPayload;
}
}
The CosmosClient is now created with our own NewtonsoftJsonCosmosSerializer and EventJsonConverter.
public class CosmosDbTests
{
[Fact]
public async Task TestFetchingDerivedTypes()
{
var endpoint = "";
var authKey = "";
var databaseId ="";
var containerId="";
var client = new CosmosClient(endpoint, authKey, new CosmosClientOptions
{
Serializer = new NewtonsoftJsonCosmosSerializer(new JsonSerializerSettings
{
Converters = { new EventJsonConverter() }
})
});
var container = client.GetContainer(databaseId, containerId);
await container.CreateItemAsync(new UserCreated{ Id = Guid.NewGuid().ToString() });
await container.CreateItemAsync(new UserUpdated{ Id = Guid.NewGuid().ToString() });
var queryable = container.GetItemLinqQueryable<IEvent>();
var query = queryable.ToFeedIterator();
var list = new List<IEvent>();
while (query.HasMoreResults)
{
list.AddRange(await query.ReadNextAsync());
}
Assert.NotEmpty(list);
}
}
I am porting a PHP/CI API that uses $params = $this->uri->uri_to_assoc() so that it can accept GET requests with many combinations, such as:
https://server/properties/search/beds/3/page/1/sort/price_desc
https://server/properties/search/page/2/lat/34.1/lon/-119.1
https://server/properties/search
etc
With lots of code like:
$page = 1;
if (!empty($params['page'])) {
$page = (int)$params['page'];
}
The two ASP.NET Core 2.1 techniques I've tried both seem like a kludge so I would appreciate any guidance on a better solution:
1) Conventional routing with catchall:
app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Properties}/{action=Search}/{*params}"
);
});
But now I have to parse the params string for the key/value pairs and am not able to take advantage of model binding.
2) Attribute routing:
[HttpGet("properties/search")]
[HttpGet("properties/search/beds/{beds}")]
[HttpGet("properties/search/beds/{beds}/page/{page}")]
[HttpGet("properties/search/page/{page}/beds/{beds}")]
public IActionResult Search(int beds, double lat, double lon, int page = 1, int limit = 10) {
}
Obviously putting every combination of allowed search parameters and values is tedious.
Changing the signature of these endpoints is not an option.
FromPath value provider
What you are wanting is to bind a complex model to part of the url path. Unfortunately, ASP.NET Core does not have a built-in FromPath binder. Fortunately, though, we can build our own.
Here is an example FromPathValueProvider in GitHub that has the following result:
Basically, it is binding domain.com/controller/action/key/value/key/value/key/value. This is different than what either the FromRoute or the FromQuery value providers do.
Use the FromPath value provider
Create a route like this:
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*path}"
);
Add the [FromPath] attribute to your action:
public IActionResult Search([FromPath]BedsEtCetera model)
{
return Json(model);
}
And magically it will bind the *path to a complex model:
public class BedsEtCetera
{
public int Beds { get; set; }
public int Page { get; set; }
public string Sort { get; set; }
}
Create the FromPath value provider
Create a new attribute based on FromRoute.
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property,
AllowMultiple = false, Inherited = true)]
public class FromPath : Attribute, IBindingSourceMetadata, IModelNameProvider
{
/// <inheritdoc />
public BindingSource BindingSource => BindingSource.Custom;
/// <inheritdoc />
public string Name { get; set; }
}
Create a new IValueProviderFactory base on RouteValueProviderFactory.
public class PathValueProviderFactory : IValueProviderFactory
{
public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
{
var provider = new PathValueProvider(
BindingSource.Custom,
context.ActionContext.RouteData.Values);
context.ValueProviders.Add(provider);
return Task.CompletedTask;
}
}
Create a new IValueProvider base on RouteValueProvider.
public class PathValueProvider : IValueProvider
{
public Dictionary<string, string> _values { get; }
public PathValueProvider(BindingSource bindingSource, RouteValueDictionary values)
{
if(!values.TryGetValue("path", out var path))
{
var msg = "Route value 'path' was not present in the route.";
throw new InvalidOperationException(msg);
}
_values = (path as string).ToDictionaryFromUriPath();
}
public bool ContainsPrefix(string prefix) => _values.ContainsKey(prefix);
public ValueProviderResult GetValue(string key)
{
key = key.ToLower(); // case insensitive model binding
if(!_values.TryGetValue(key, out var value)) {
return ValueProviderResult.None;
}
return new ValueProviderResult(value);
}
}
The PathValueProvider uses a ToDictionaryFromUriPath extension method.
public static class StringExtensions {
public static Dictionary<string, string> ToDictionaryFromUriPath(this string path) {
var parts = path.Split('/');
var dictionary = new Dictionary<string, string>();
for(var i = 0; i < parts.Length; i++)
{
if(i % 2 != 0) continue;
var key = parts[i].ToLower(); // case insensitive model binding
var value = parts[i + 1];
dictionary.Add(key, value);
}
return dictionary;
}
}
Wire things together in your Startup class.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddMvcOptions(options =>
options.ValueProviderFactories.Add(new PathValueProviderFactory()));
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMvc(routes => {
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*path}"
);
});
}
}
Here is a working sample on GitHub.
Edit
My other answer is a better option.
General Idea
$params = $this->uri->uri_to_assoc() turns a URI into an associative array, which is basically a .NET Dictionary<TKey, TValue>. We can do something similar in ASP.NET Core. Lets say we have the following routes.
app.UseMvc(routes => {
routes.MapRoute(
name: "properties-search",
template: "{controller=Properties}/{action=Search}/{*params}"
);
});
Bind Uri Path to Dictionary
Action
public class PropertiesController : Controller
{
public IActionResult Search(string slug)
{
var dictionary = slug.ToDictionaryFromUriPath();
return Json(dictionary);
}
}
Extension Method
public static class UrlToAssocExtensions
{
public static Dictionary<string, string> ToDictionaryFromUriPath(this string path) {
var parts = path.Split('/');
var dictionary = new Dictionary<string, string>();
for(var i = 0; i < parts.Length; i++)
{
if(i % 2 != 0) continue;
var key = parts[i];
var value = parts[i + 1];
dictionary.Add(key, value);
}
return dictionary;
}
}
The result is an associative array based on the URI path.
{
"beds": "3",
"page": "1",
"sort": "price_desc"
}
But now I have to parse the params string for the key/value pairs and am not able to take advantage of model binding.
Bind Uri Path to Model
If you want model binding for this, then we need to go a step further.
Model
public class BedsEtCetera
{
public int Beds { get; set; }
public int Page { get; set; }
public string Sort { get; set; }
}
Action
public IActionResult Search(string slug)
{
BedsEtCetera model = slug.BindFromUriPath<BedsEtCetera>();
return Json(model);
}
Additional Extension Method
public static TResult BindFromUriPath<TResult>(this string path)
{
var dictionary = path.ToDictionaryFromUriPath();
var json = JsonConvert.SerializeObject(dictionary);
return JsonConvert.DeserializeObject<TResult>(json);
}
IMHO you are looking at this from the wrong perspective.
Create a model:
public class FiltersViewModel
{
public int Page { get; set; } = 0;
public int ItemsPerPage { get; set; } = 20;
public string SearchString { get; set; }
public string[] Platforms { get; set; }
}
API Endpoint:
[HttpGet]
public async Task<IActionResult> GetResults([FromRoute] ViewModels.FiltersViewModel filters)
{
// process the filters here
}
Result Object (dynamic)
public class ListViewModel
{
public object[] items;
public int totalCount = 0;
public int filteredCount = 0;
}
I'm trying to get a list of documents from documentdb of specific object type -
_client.CreateDocumentQuery<RuleSetGroup>(_collectionLink)
.Where(f => f.SourceSystemId == sourceSystemId).AsEnumerable().ToList();
This returns objects of types other than RuleSetGroup, as long as they have a property SourceSystemId matching what I pass in. I understand this is how documentdb works, is there a way to enforce the type T so only those objects are returned?
I am using Auto Type Handling:
JsonConvert.DefaultSettings = () => new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto
};
You will get different document types unless you implement a Type Pattern (adding a Type attribute to each Class) and use it as extra filter.
The reason is because you are storing NoSQL documents, which can obviously have different schema. DocumentDB treats them all equally, they are all documents; when you query, it's your responsability (because only you know the difference) to separate the different document types.
If you document Types all have an attribute "Client" (for example, Orders and Invoices) and you create a query with that attribute but mapped to one Type (Orders), you will get both Orders and Invoices that match the filter because they are documents that match the query. The deserialization logic is on your end, not within DocDB.
Here is an article regarding that Type Pattern when storing different document Types on DocDB (check the Base Type Pattern section).
Something like this might solve it:
public abstract class Entity
{
public Entity(string type)
{
this.Type = type;
}
/// <summary>
/// Object unique identifier
/// </summary>
[Key]
[JsonProperty("id")]
public string Id { get; set; }
/// <summary>
/// Object type
/// </summary>
public string Type { get; private set; }
}
public class RuleSetGroup : Entity
{
public RuleSetGroup():base("rulesetgroup")
}
public class OtherType : Entity
{
public OtherType():base("othertype")
}
_client.CreateDocumentQuery<RuleSetGroup>(_collectionLink).Where(f => f.Type == "rulesetgroup" && f.SourceSystemId == sourceSystemId).AsEnumerable().ToList();
You can wrap queries on helpers that set the type as a Where clause before applying your other filters (in LINQ you can chain Wheres without problem).
My repository might be a little too much for you, the short answer is that you can return .AsDocumentQuery() instead of .ToList()
public async Task<IEnumerable<T>> GetDocumentsAsync<T>(Expression<Func<T, bool>> predicate, int maxReturnedDocuments = -1,
bool enableCrossPartitionQuery = true, int maxDegreeOfParallellism = -1, int maxBufferedItemCount = -1)
{
//MaxDegreeofParallelism default = 0, add -1 to let SDK handle it instead of a fixed 1 network connection
var feedOptions = new FeedOptions
{
MaxItemCount = maxReturnedDocuments,
EnableCrossPartitionQuery = enableCrossPartitionQuery,
MaxDegreeOfParallelism = maxDegreeOfParallellism,
MaxBufferedItemCount = maxBufferedItemCount
};
IDocumentQuery<T> query = client.CreateDocumentQuery<T>(
UriFactory.CreateDocumentCollectionUri(_databaseName, _collectionName), feedOptions)
.Where(predicate)
.AsDocumentQuery();
List<T> results = new List<T>();
while (query.HasMoreResults)
{
var res = await query.ExecuteNextAsync<T>();
results.AddRange(res);
}
return results;
}
You can call the above method like this:
var ecsterConfigs = await repoBO.GetDocumentsAsync<EcsterPaymentConfig>(c => c.ValidTo == null && c.Type == type);
And then I have a wrapper around it sometimes when I "might" do an update of document, to keep track of the _Etag which will change if there is another update on the document before I write it down again.
public class DocumentWrapper<DocumentType>
{
public DocumentWrapper(Document document)
{
Value = (DocumentType)(dynamic)document;
ETag = document.ETag;
TimeStamp = document.Timestamp;
}
public DocumentType Value { get; set; }
public string ETag { get; set; }
public DateTime TimeStamp { get; set; }
}
#Granlund how do you make GetDocumentsAsync return DocumentWrapper instances while still allowing the predicate to query on the properties of the Value?
Here is what I came up with but maybe you have a better way:
[TestMethod]
[TestCategory("CosmosDB.IntegrationTest")]
public async Task AddAndReadDocumentWrapperViaQueryAsync()
{
var document = new Foo { Count = 1, Name = "David" };
var response = await client.CreateDocumentAsync(documentCollectionUri, document);
var id = response.Resource.Id;
var queryResult = await GetWrappedDocumentsAsync<Foo>(f => f.Where(a => a.Name == "David"));
foreach (var doc in queryResult)
{
Assert.AreEqual("David", doc.Value.Name);
}
}
public class Foo
{
public int Count { get; set; }
public string Name { get; set; }
}
public class DocumentWrapper<DocumentType>
{
public DocumentWrapper(Document document)
{
Value = (DocumentType)(dynamic)document;
ETag = document.ETag;
TimeStamp = document.Timestamp;
}
public DocumentType Value { get; set; }
public string ETag { get; set; }
public DateTime TimeStamp { get; set; }
}
public async Task<IEnumerable<DocumentWrapper<T>>> GetWrappedDocumentsAsync<T>(
Func<IQueryable<T>, IQueryable<T>> query,
int maxReturnedDocuments = -1,
bool enableCrossPartitionQuery = true,
int maxDegreeOfParallellism = -1,
int maxBufferedItemCount = -1)
{
//MaxDegreeofParallelism default = 0, add -1 to let SDK handle it instead of a fixed 1 network connection
var feedOptions = new FeedOptions
{
MaxItemCount = maxReturnedDocuments,
EnableCrossPartitionQuery = enableCrossPartitionQuery,
MaxDegreeOfParallelism = maxDegreeOfParallellism,
MaxBufferedItemCount = maxBufferedItemCount
};
IDocumentQuery<T> documentQuery =
query(client.CreateDocumentQuery<T>(documentCollectionUri, feedOptions)).AsDocumentQuery();
var results = new List<DocumentWrapper<T>>();
while (documentQuery.HasMoreResults)
{
var res = await documentQuery.ExecuteNextAsync<Document>();
results.AddRange(res.Select(d => new DocumentWrapper<T>(d)));
}
return results;
}
This is the simplified model :
public class Person {
public int Id;
public string Name;
}
public class Task {
public int Id;
public int PersonId;
public DateTime StartDate;
[GreaterThan("StartDate")]
public DateTime EndDate;
}
To validate the EndDate >= StartDate, i write general GreaterThanAttribute. The server side is trivial, but i have problem on client side validation.
My GreaterThanAttribute got the other property (ex: "StartDate") from constructor, then i pass this other property name to javascript as validation rule. But it won't work, because the JS will not found this element, because MVC will render & named it as "Task.StartDate", not "StartDate".
My question is, how i can get the prefix which will be used by the controller to render my model inside IClientValidatable.GetClientValidationRules() ?
Thanks
Here's how you could implement it:
public class GreaterThanAttribute : ValidationAttribute, IClientValidatable
{
private readonly string _otherProperty;
public GreaterThanAttribute(string otherProperty)
{
_otherProperty = otherProperty;
}
public override string FormatErrorMessage(string name)
{
return string.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _otherProperty);
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_otherProperty);
if (property == null)
{
return new ValidationResult(
string.Format(
CultureInfo.CurrentCulture,
"unknown property {0}",
_otherProperty
)
);
}
var otherValue = (DateTime)property.GetValue(validationContext.ObjectInstance, null);
var thisValue = (DateTime)value;
if (thisValue <= otherValue)
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule();
rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
rule.ValidationType = "greaterthandate";
rule.ValidationParameters["other"] = "*." + _otherProperty;
yield return rule;
}
}
and on the client side:
(function ($) {
var getModelPrefix = function (fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf('.') + 1);
};
var appendModelPrefix = function (value, prefix) {
if (value.indexOf('*.') === 0) {
value = value.replace('*.', prefix);
}
return value;
};
$.validator.unobtrusive.adapters.add('greaterthandate', ['other'], function (options) {
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(':input[name=' + fullOtherName + ']')[0];
options.rules['greaterThanDate'] = element;
if (options.message) {
options.messages['greaterThanDate'] = options.message;
}
});
$.validator.addMethod('greaterThanDate', function (value, element, params) {
var otherDate = $(params).val();
var thisDate = value;
// TODO: put your custom date comparison implementation here between
// the 2 values. Be careful here. Javascript date handling is culture dependent
// so you might need to account for that when building your js Date instances
return false;
}, '');
})(jQuery);
This has already been done. I suggest you use FoolProof validation. If you do not want to, you can at least check out their soure code. Here's a link