I want to do a project with JSON Web Signature (JWS) and i want to send the public key of the certificate used for the signature so that the message can be validated once received with this public key. I am using the Ninbus JOS JWT library. I can sign the JSON object and I can see the public key, but i can not validate it correctly.
This is the code:
// Create RSA-signer with the private key
JWSSigner signer = new RSASSASigner(_signatureKey_); // PrivateKey
com.nimbusds.jose.util.Base64 b64 = new com.nimbusds.jose.util.Base64(_x509certificate.toString()); // X509Certificate
ArrayList<com.nimbusds.jose.util.Base64> certificados = new ArrayList<com.nimbusds.jose.util.Base64>();
certificados.add(b64);
RSAPublicKey _rsaPublicKey = (RSAPublicKey)_x509certificate.getPublicKey(); // Get the public key of the X509Certificate
RSAKey jwk = new com.nimbusds.jose.jwk.RSAKey.Builder( new Base64URL( _rsaPublicKey.getModulus().toString()), new Base64URL( _rsaPublicKey.getPublicExponent().toString()))
.x509CertChain(certificados)
.build();
JWSHeader _jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).
x509CertChain(certificados).
jwk(jwk).
build();
// Prepare JWS object with simple string as payload
JWSObject jwsObject = new JWSObject(_jwsHeader, new Payload(_jsonObject));
// Compute the RSA signature
jwsObject.sign(signer);
// Validation OK : This validation works
JWSVerifier verifier = new RSASSAVerifier(_rsaPublicKey);
boolean signatureValid = jwsObject.verify(verifier); // ---> True, OK
// Now I want to validate the JWSObject getting the public key from the same JWSObject. This validation Fails
JWK _jwk = jwsObject.getHeader().getJWK();
RSAKey _rsakey = (RSAKey)_jwk;
RSAPublicKey _rsaPublicKey2 = _rsakey.toRSAPublicKey();
JWSVerifier verifier2 = new RSASSAVerifier(_rsakey.toRSAPublicKey());
boolean verificado2 = jwsObject.verify(verifier2); // False!
// Another option, this fails too
RSAKey __rsaKey2 = new com.nimbusds.jose.jwk.RSAKey.Builder( _rsakey.toRSAPublicKey() ).x509CertChain(_jwk.getX509CertChain()).build();
JWSVerifier verifier3 = new RSASSAVerifier(__rsaKey2);
boolean verificado3 = jwsObject.verify(verifier3); // False!
The _rsaPublicKey is: "Sun RSA public key, 2048 bits", but when i get it from the JWK (_rsaPublicKey2), I get "Sun RSA public key, 3696 bits" and i don't know why.
Thanks!
On the recipient side, do you validate the X.509 certificate issuer, subject and chain before trusting the key? Signature validation must not be attempted before the recipient is certain that it can trust the certificate included in the JWS.
Another note: Do not include the public JWK in the JWS header. This should only be used for ephemeral public keys in ECDH (a different alg used for JWE). Passing the certificate chain in the JWS header is sufficient, but you must validate it / find out if the certificate can be trusted, before using its public key.
The library will not validate / find out if the certificate can be trusted for you!
If the second signature validation fails, then probably the key used to sign the JWS and the one that came with the X.509 certificate are not the same (as suggested by the different reported lengths - 2048 bits vs. 3696 bits).
Related
I have a question regarding Restful services
I need to upload CSV files to an AWS server. I am registered with account.
First I need to obtain an access token and use that to upload the files. I have not coded anything yet, trying to understand the best approach, I so hope to use Camel-Rest-DSL. It is required to communicate with JSON. But, the authentication part has me stuck, I’m pretty sure it uses OAuth2 auth, RestFul web service and JSON, this should just be a client, I was looking at WSS4J for JAX-RS OAuth2 but I don’t know.
I’ve done it with postman, this is the scenario. The username and password are fictional
*Get Access Token
uses POST verb
requires Token Request URL
uses Basic Auth requires Username = Client ID of tenant ( needs to be encoded base64 )
HEADER parm Content-Type = x-www-form-urlencoded
Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic ' + encoded Client ID
Access Token body - grant_type, username, password
Body = username = 5c0642fe-a495-44db-93f7-67034556fa2c061_ingestor
password = 154f0252d166f27b5e21ef171b03212a79f41a0daf3
grant_type = password
#returns the access_token as JSON
POST or upload files
uses POST verb
requires Ingestion URL UploadURL
UploadURL=https://apm-ts-query-svc-prd.app-api.aws-usw02-pr.something.io/v2/time_series/
UploadFolder=upload
headers =
key Authentication "Bearer + access Token" (from request access token above)
key Tenant = TenantUUID
key X-GE-CsvFormat = ODB
# Body
form-data
key file=file
# POST DATA
headers content-type application/json
authorization: "" + token
tenant: "" + tenant
My environment
Jboss Fuse 6.3-310
Karaf version 2.4.0.redhat-630310
JVM
Java Virtual Machine Java HotSpot(TM) 64-Bit Server VM version 25.162-
b12
Version 1.8.0_162
Vendor Oracle Corporation
Operating system
Name Linux version 2.6.32-696.20.1.el6.x86_64
I can't use OAuth2/SAML assertions so I will simply request a token and cache it and use it later. this is my test code
#Override
public void configure() throws Exception {
//Configure the Rest Web Service Component transport to use a REST implementation
restConfiguration() //Configures the REST DSL to use a specific REST implementation
.component("jetty") //Specifies the Camel component to use as the REST transport
.host("localhost") //The hostname to use for exposing the REST service
.port("8282") //The port number to use for exposing the REST service JMX tooling
.scheme("https") //The protocol to use for exposing the REST service
.contextPath("/oauth/token") //Sets a leading context path for the REST services
.bindingMode(RestBindingMode.json) //Enables binding mode for JSON
.jsonDataFormat("json-jackson") //Specifies the component that Camel uses to implement the JSON data format
.dataFormatProperty("prettyPrint", "true"); //set arbitrary properties on the underlying data format component
//Configure the Rest Endpoint
rest("/oauth") //Defines a service using the REST DSL. Each of the verb clauses are terminated by a to() keyword,
//which forwards the incoming message to an endpoint
.post("/token")
.produces("application/json")
.consumes("application/json")
.type(TokenEntities.class)
.route()
.routeId("Get Auth Token Route")
.autoStartup(true)
.id("Get Auth Token Service")
.description("Get Authorization Token")
.process(new UAARequestTokenProcessor())
.to("https://d1e53858-2903-4c21-86c0-95edc7a5cef2.pager-uaa.run.aws-usw02-pr.ice.pager.io/oauth/token")
.to("log:logger?showBody=true")
.to("direct:accessToken")
.endRest();
//Define the Route - from() Defines a regular Camel route.
from("direct:accessToken").to("log:logger?showBody=true"); }
public class UAARequestTokenProcessor implements Processor {
private static final Logger LOG = LoggerFactory.getLogger(UAARequestTokenProcessor.class);
private String clientId = "myClientID";
private String userName = "myUserName";
private String password = "myPassword";
#Override
public void process(Exchange exchange) throws Exception {
LOG.info("Processing UAA token request for " + clientId + " and " + userName);
Message msg = exchange.getOut(); //create outbound message exchange
StringBuilder authHeader = new StringBuilder("Basic ");
authHeader.append(Base64.getEncoder().encodeToString((clientId + ":").getBytes("UTF_8")));
String body = String.format("grant_type=password&username=%s&password=%s",
URLEncoder.encode(userName, "UTF-8"), //Translates a string into x-www-form-urlencoded format
URLEncoder.encode(password, "UTF-8"));
msg.setHeader(Exchange.CONTENT_TYPE, "MediaType.APPLICATION_FORM_URLENCODE");
msg.setHeader("Authorization", authHeader.toString());
msg.setBody(body);
}
}
I am getting this error every time I try to post data to my server:
Server logs:
Starting the internal [HTTP/1.1] server on port 9192
Starting facilitymanager.api.rest.FacilityManagerAPIRestWrapper application
2015-06-22 13:18:11 127.0.0.1 - - 9192 POST /devices/rename - 415 554 45 64 http://localhost:9192 Java/1.7.0_79 -
Stopping the internal server
However In the service handler I am stating that I will handle JSON messages as you can see here:
public static final class RenameDevice extends ServerResource {
#Post("application/json")
public String doPost() throws InterruptedException, ConstraintViolationException, InvalidChoiceException, JSONException {
configureRestForm(this);
final String deviceId = getRequest().getAttributes().get("device_id").toString();
final String newName = getRequest().getAttributes().get("new_name").toString();
return renameDevice(deviceId, newName).toString(4);
}
}
/**
* Enables incoming connections from different servers.
*
* #param serverResource
* #return
*/
#SuppressWarnings({ "unchecked", "rawtypes" })
private static Series<Header> configureRestForm(ServerResource serverResource) {
Series<Header> responseHeaders = (Series<Header>) serverResource.getResponse().getAttributes()
.get("org.restlet.http.headers");
if (responseHeaders == null) {
responseHeaders = new Series(Header.class);
serverResource.getResponse().getAttributes().put("org.restlet.http.headers", responseHeaders);
}
responseHeaders.add("Access-Control-Allow-Origin", "*");
responseHeaders.add("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
responseHeaders.add("Access-Control-Allow-Headers", "Content-Type");
responseHeaders.add("Access-Control-Allow-Credentials", "false");
responseHeaders.add("Access-Control-Max-Age", "60");
return responseHeaders;
}
What am I missing here?
Thanks!
Edit: This is the full log concerning the request:
Processing request to: "http://localhost:9192/devices/rename"
Call score for the "org.restlet.routing.VirtualHost#54594d1d" host: 1.0
Default virtual host selected
Base URI: "http://localhost:9192". Remaining part: "/devices/rename"
Call score for the "" URI pattern: 0.5
Selected route: "" -> facilitymanager.api.rest.FacilityManagerAPIRestWrapper#d75d3d7
Starting facilitymanager.api.rest.FacilityManagerAPIRestWrapper application
No characters were matched
Call score for the "/devices/list" URI pattern: 0.0
Call score for the "/groups/rename" URI pattern: 0.0
Call score for the "/devices/rename" URI pattern: 1.0
Selected route: "/devices/rename" -> Finder for RenameDevice
15 characters were matched
New base URI: "http://localhost:9192/devices/rename". No remaining part to match
Delegating the call to the target Restlet
Total score of variant "[text/html]"= 0.25
Total score of variant "[application/xhtml+xml]"= 5.0E-4
Converter selected for StatusInfo: StatusInfoHtmlConverter
2015-06-22 13:28:31 127.0.0.1 - - 9192 POST /devices/rename - 415 554 45 67 http://localhost:9192 Java/1.7.0_79 -
POST /devices/rename HTTP/1.1 [415 Unsupported Media Type] ()
KeepAlive stream used: http://localhost:9192/devices/rename
sun.net.www.MessageHeader#2bf4dee76 pairs: {null: HTTP/1.1 415 Unsupported Media Type}{Content-type: text/html; charset=UTF-8}{Content-length: 554}{Server: Restlet-Framework/3.0m1}{Accept-ranges: bytes}{Date: Mon, 22 Jun 2015 12:28:31 GMT}
To obtain a full log one must invoke this line of code anywhere before opening the restlet/component server:
// Create a new Component.
component = new Component();
// Add a new HTTP server listening on default port.
component.getServers().add(Protocol.HTTP, SERVER_PORT);
Engine.setLogLevel(Level.ALL); /// <----- HERE
component.start();
I've found the problem! The thing is that a tagged #Post method must receive an argument.
So the method should be like this:
#Post("application/json")
public String doPost(Representation entity) throws InterruptedException, ConstraintViolationException,
InvalidChoiceException, JSONException, IOException {
configureRestForm(this);
final Reader r = entity.getReader();
StringBuffer sb = new StringBuffer();
int c;
// Reads the JSON from the input stream
while ((c = r.read()) != -1) {
sb.append((char) c);
}
System.out.println(sb.toString()); // Shows the JSON received
}
}
The Representation entity argument brings you the means to detect the media type you are receiving. But since I have my tag like #Post("application/json") I do not need to verify this again.
Imagine that I use just "#Post" instead of "#Post("application/json")", I would have to validate the media type (or types) this way:
#Post
public Representation doPost(Representation entity)
throws ResourceException {
if (entity.getMediaType().isCompatible(MediaType.APPLICATION_JSON)) {
// ...
}
// ...
}
A method with an #Post annotation is not required to receive an argument, unless you intend to receive a payload from your request.
If you want to filter on the media type of the incoming representation, use the "json" shortcut, as follow
#Post("json")
This will prevent you to test the media type of the representation.
The list of all available shortcut is available here. Most of them are quite simple to remember. The main reason to use shortcuts (or "extension" such as file extension) is that "xml" is related to several media types (application/xml, text/xml).
If you want to get the full content of the representation, simply call the "getText()" method, instead of using the getReader() and consume it.
If you want to support CORS, I suggest you to use the CorsService (available in the 2.3 version of the Restlet Framework.
Notice there exists a shortcut for getting the headers from a Request or a Response, just call the "getHeaders()" method.
Notice there exists a shortcut for getting the attributes taken from the URL, just call the "getAttribute(String) method.
Here is an updated version of your source code:
public class TestApplication extends Application {
public final static class TestPostResource extends ServerResource {
#Post
public String doPost(Representation entity) throws Exception {
final String deviceId = getAttribute("device_id");
final String newName = getAttribute("new_name");
System.out.println(entity.getText());
System.out.println(getRequest().getHeaders());
System.out.println(getResponse().getHeaders());
return deviceId + "/" + newName;
}
}
public static void main(String[] args) throws Exception {
Component c = new Component();
c.getServers().add(Protocol.HTTP, 8183);
c.getDefaultHost().attach(new TestApplication());
CorsService corsService = new CorsService();
corsService.setAllowedOrigins(new HashSet<String>(Arrays.asList("*")));
corsService.setAllowedCredentials(true);
corsService.setSkippingResourceForCorsOptions(true);
c.getServices().add(corsService);
c.start();
}
#Override
public Restlet createInboundRoot() {
Router router = new Router(getContext());
router.attach("/testpost/{device_id}/{new_name}", TestPostResource.class);
return router;
}
}
I have problems using Crypto++ to save a RSA public key (that I obtained loading a private key file in PKCS#8 format). When decoding the key, I always get a BERDecodeErr exception.
Here is the code I am using:
CryptoPP::RSASSA_PKCS1v15_SHA_Signer _signer;
CryptoPP::RSASSA_PKCS1v15_SHA_Verifier _verifier;
CryptoPP::ByteQueue bytes;
//_signer.AccessPublicKey().Save(bytes); // seem to save private key instead
_signer.AccessKey().DEREncodePublicKey(bytes);
//_verifier.AccessKey().Load(bytes);
//_verifier.AccessKey().BERDecodePublicKey(bytes, 0, 0);
_verifier.AccessPublicKey().Load(bytes);
I also tried with the instructions commented above, without success.
How do you do to save or open the public key?
The public key looks like this in hex format, is there a tool to check its format / validity (regarding what crypto++ supports) ?
3081890281810097e24f2e95504a397e90fbc56d1b330ab2ab97a0d326007b890e40013f9e1d9bd9
f54b0c0840782ddae19b5b4595d8f8b9ffe0d2120174fcbc39585c5867cd2dfba69f8e540caa2c52
de8f08278a34e9249120500117f0ba756c5bb2be660013160db9f82f75deb7ccf63742a9e945da6c
cf30c2b109b73342daaabd02b872e50203010001
I'm not sure I understand your problem completely. But you look like you are on the right track with using either Load/Save or BERDecodePublicKey/DEREncodePublicKey.
Here's how I would approach it given you have a PKCS#8 encoded private key.
FileSource privateKey("<private key>", true);
RSASSA_PKCS1v15_SHA_Signer signer;
signer.AccessKey().Load(privateKey);
AutoSeededRandomPool prng;
bool valid = signer.AccessKey().Validate(prng, 3);
...
RSASSA_PKCS1v15_SHA_Verifier verifier(signer);
FileSink publicKey("<public key>", true);
verifier.AccessKey().Save(publicKey);
Then, you can use Gutmann's dumpasn1 to print it:
$ dumpasn1 <public key>
...
I believe you can also convert a private key/signer to a public key/verifier with:
RSASSA_PKCS1v15_SHA_Signer signer;
signer.AccessKey().Load(privateKey);
RSASSA_PKCS1v15_SHA_Verifier verifier;
signer.MakePublic(verifier);
There's also a page on the Crypto++ wiki that talks about it in greater detail: Keys and Formats. And there's a page dedicated to PEM encoding, if interested: PEM Pack. If you want the PEM encoding, you have to compile the library yourself, though.
Here's the code I used with the public key you posted. It had no problems.
string key = "3081890281810097e24f2e95504a397e90fbc56d1b330ab2ab97a0d326007b890e40013f9e1d9bd9 \
f54b0c0840782ddae19b5b4595d8f8b9ffe0d2120174fcbc39585c5867cd2dfba69f8e540caa2c52 \
de8f08278a34e9249120500117f0ba756c5bb2be660013160db9f82f75deb7ccf63742a9e945da6c \
cf30c2b109b73342daaabd02b872e50203010001";
ByteQueue queue;
StringSource ss(key, true, new HexDecoder(new Redirector(queue)));
RSASSA_PKCS1v15_SHA_Verifier verifier;
verifier.AccessKey().BERDecodePublicKey(queue, false, 0);
AutoSeededRandomPool prng;
bool result = verifier.AccessKey().Validate(prng, 3);
if(!result)
throw Exception(Exception::OTHER_ERROR, "Failed to validate public key");
If you install the PEM Pack then you can add the following:
FileSink sink("public-key.pem", true);
PEM_Save(sink, verifier.GetKey());
That will get you:
$ cat public-key.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCX4k8ulVBKOX6Q+8VtGzMKsquX
oNMmAHuJDkABP54dm9n1SwwIQHgt2uGbW0WV2Pi5/+DSEgF0/Lw5WFxYZ80t+6af
jlQMqixS3o8IJ4o06SSRIFABF/C6dWxbsr5mABMWDbn4L3Xet8z2N0Kp6UXabM8w
wrEJtzNC2qq9Arhy5QIDAQAB
-----END PUBLIC KEY-----
And:
$ openssl rsa -in public-key.pem -pubin -text -noout
Public-Key: (1024 bit)
Modulus:
00:97:e2:4f:2e:95:50:4a:39:7e:90:fb:c5:6d:1b:
33:0a:b2:ab:97:a0:d3:26:00:7b:89:0e:40:01:3f:
9e:1d:9b:d9:f5:4b:0c:08:40:78:2d:da:e1:9b:5b:
45:95:d8:f8:b9:ff:e0:d2:12:01:74:fc:bc:39:58:
5c:58:67:cd:2d:fb:a6:9f:8e:54:0c:aa:2c:52:de:
8f:08:27:8a:34:e9:24:91:20:50:01:17:f0:ba:75:
6c:5b:b2:be:66:00:13:16:0d:b9:f8:2f:75:de:b7:
cc:f6:37:42:a9:e9:45:da:6c:cf:30:c2:b1:09:b7:
33:42:da:aa:bd:02:b8:72:e5
Exponent: 65537 (0x10001)
Finally, the difference between:
verifier.AccessKey(): gets the RSA::Public key, the key is non-const
verifier.GetKey(): gets the RSA::Public key, the key is const
I am getting a 415 Error when sending a form entry to another client resource via JSON. The target URI in my code below ("/message") works when not using the form (i.e. hit "/message" with a test mock object).
Here is my code to get the values of the form and do the post to the target resource. Am I missing something that needs to be done?
I am using the following:
Restlet: 2.1 RC5
GAE: 1.6.1
Form Restlet:
#Post
public void handlePost(Representation entity) {
final Form webForm = new Form(entity);
MessageEntity newMessage = new MessageEntity();
String subject = webForm.getFirstValue("subject");
String sendto = webForm.getFirstValue("email");
String message = webForm.getFirstValue("message");
newMessage.setCategoryID(subject);
newMessage.setAccountID(sendto);
newMessage.setMessageText(message);
ClientResource cr = new ClientResource(getRootRef()+ "/message");
cr.post(newMessage, MediaType.APPLICATION_JSON);
}
Target Resource ("/message")
#Post("json")
public void HandleRequest(MessageEntity messageEntity) {
// Logic here
}
Please let me know if you need more information
Thanks!
I have code that is very similar to yours that works fine. I am also running similar versions of Restlet and GAE. First question I have is are there other #Post methods in your Target Resource as sometimes the ordering matters.
Here are two versions of code that I have that work....
1)
public Representation postHandler() {
Reference commitsRef = new Reference(Consts.RESOURCE_BASE + "commitments/");
ClientResource commitsResource = new ClientResource(getContext(), commitsRef);
....
Representation commitsRep = commitsResource.post(commitForm);
That is posting a form to a Target resource that handles both #Post("json") and #Post("form")
2)
public Representation doPostFromGet() {
Reference takeActRef = new Reference(Consts.RESOURCE_BASE + "commitment/"
+ commitmentId + "/userActs/");
ClientResource takeActResource = new ClientResource(getContext(), takeActRef);
...
Representation takeActRep = takeActResource.post(newAct);
That is posting a Java object to a form that uses what I call the "Peierls magic". See:
http://tembrel.blogspot.com/2012/03/converting-forms-in-restlet-to-pojos.html
It allows you to have one post() in the Target and accept both forms and pojos.
On a minor note, if you are doing a post to add a new message, should the url be "/messages/" (plural) - and perhaps there is a typo somewhere? (An unlikely possibility, but I thought I would mention it).
Good luck,
RB
The scenarios:
3 tables I have which form a Many-to-many relationship:
Agent (AgentID (PK), AgentName)
AgentChannel (AgentID (PK), ChannelID (PK))
Channel (ChannelID (PK), ChannelName)
I do not want to add/delete any record in Agent and Channel tables, but only modify the AgentChannel table.
I am using EF 4.1, POCO and Self-Tracking Entity. All codes that deal with database are include in WCF layer which I calls them server side code, and I only able to control the add/remove of records in client side code.
For example:
I has one Agent record, with multiple Channel records. I able to link existing channel to agent in this way:
var channel = new Channel { ChannelID = 1 };
channel.MarkAsUnchanged();
agent.Channels.Add(channel);
// This will add new entry to AgentChannel table, but no change on Agent and Channel tables
Also I can remove the linkage of a channel to agent in this way:
var tempChannels = agent.Channels.ToList();
var channelToDelete = tempChannels.FirstOrDefault(c => c.ChannelID == 3);
agent.Channels.Remove(channelToDelete);
// This will remove the entry in AgentChannel, but no change on Agent and Channel tables.
My problem:
If I removed a channel, and add back a new channel which has the same ChannelID with the previously removed channel into agent.Channels collection , I will get this error:
AcceptChanges cannot continue because the object’s key values conflict with another object in the ObjectStateManager.
Sample code to demostrates:
//
// I not able to call ObjectContext here because it was reside in server side.
//
//
// This is client side code
//
var tempChannels = agent.Channels.ToList();
var channelToDelete = tempChannels.FirstOrDefault(c => c.ChannelID == 3);
// remove channel 3
agent.Channels.Remove(channelToDelete);
var channel = new Channel { ChannelID = 3 };
channel.MarkAsUnchanged();
// Add back channel 3
agent.Channels.Add(channel);
//
// This is server side code (WCF layer)
//
try
{
using (var ctx = new testEntities())
{
ctx.ApplyChanges("Agents", agent); // <-- ERROR happen here
ctx.SaveChanges();
}
}
catch (Exception)
{
throw;
}
then I got thie exception message:
System.InvalidOperationException was unhandled
Message=AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges.
Source=System.Data.Entity
StackTrace:
at System.Data.Objects.ObjectStateManager.FixupKey(EntityEntry entry)
at System.Data.Objects.EntityEntry.AcceptChanges()
at System.Data.Objects.EntityEntry.ChangeObjectState(EntityState requestedState)
at System.Data.Objects.ObjectStateManager.ChangeObjectState(Object entity, EntityState entityState)
at TestManyToMany.SelfTrackingEntitiesContextExtensions.ChangeEntityStateBasedOnObjectState(ObjectContext context, IObjectWithChangeTracker entity) in F:\MyFile\Development\Temp\TestManyToMany\TestManyToMany\Model1.Context.Extensions.cs:line 728
at TestManyToMany.SelfTrackingEntitiesContextExtensions.HandleEntity(ObjectContext context, EntityIndex entityIndex, RelationshipSet allRelationships, IObjectWithChangeTracker entity) in F:\MyFile\Development\Temp\TestManyToMany\TestManyToMany\Model1.Context.Extensions.cs:line 596
at TestManyToMany.SelfTrackingEntitiesContextExtensions.ApplyChanges[TEntity](ObjectContext context, String entitySetName, TEntity entity) in F:\MyFile\Development\Temp\TestManyToMany\TestManyToMany\Model1.Context.Extensions.cs:line 84
at TestManyToMany.Program.Main(String[] args) in F:\MyFile\Development\Temp\TestManyToMany\TestManyToMany\Program.cs:line 53
at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()
InnerException:
I hope I make myself clear. This is the only problem I facing now and I run out of idea.
Many thanks.
-
-
Update - Answer
I got the answer after done the testing according to comment from Ladislav Mrnka.
Basically you need to add back to the same entity which removed previously, no other entity because that will gave you the same error above.
Here is the sample code on client side, no changes required in server side:
int channelId = 1;
var tempChannels = agent.Channels.ToList();
var channelToDelete = tempChannels.FirstOrDefault(c => c.ChannelID == channelId);
// remove channel 1
agent.Channels.Remove(channelToDelete);
//var channel = _allChannels.First(c => c.ChannelID == channelId);
//agent.Channels.Add(channel); <-- This line will give you ERROR because the channel entity is from _allChannels, but not from agent.Channels
// Add back channel 1
agent.Channels.Add(channelToDelete); // <-- This is CORRECT
That is not allowed operation. If you removed channel and you want to add it again you mustn't create a new instance. You must use the old one and only change its state. The main rule is that each entity with unique key must be declared only once. Your code results in two instances with the same key = error.